diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..82cef9fc30f9afb8af56213e1a9268268521c025 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.gemini +.vercel + +backend/.env +backend/venv +backend/__pycache__ +backend/**/__pycache__ +backend/**/*.pyc + +frontend/.env +frontend/node_modules +frontend/dist + +*.log diff --git a/.gemini/antigravity/brain/8453b74f-68a6-47ae-887d-1123cb011afb/scratch/verify_supabase.py b/.gemini/antigravity/brain/8453b74f-68a6-47ae-887d-1123cb011afb/scratch/verify_supabase.py new file mode 100644 index 0000000000000000000000000000000000000000..e55e2da83811f3a419924ecfb98b195e43c59472 --- /dev/null +++ b/.gemini/antigravity/brain/8453b74f-68a6-47ae-887d-1123cb011afb/scratch/verify_supabase.py @@ -0,0 +1,10 @@ +import sys +import os +sys.path.append(os.path.join(os.getcwd(), 'backend')) + +try: + from backend.services.supabase_service import supabase + res = supabase.table("agents").select("count").execute() + print(f"Connection successful! Agents count: {res.data}") +except Exception as e: + print(f"Error connecting to Supabase: {e}") diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..a6344aac8c09253b3b630fb776ae94478aa0275b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,35 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a21957cf7f6ea03844fd2a6dced7ea422c8381b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +backend/.env +/backend/venv +/backend/__pycache__ +frontend/.env +node_modules/ +dist/ +__pycache__/ +*.pyc + +.vercel +frontend/ios +frontend/android +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4105bdf0e020db36a29398003974d4306def3d8d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +FROM node:22-slim AS frontend-build + +WORKDIR /app/frontend + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend ./ + +ARG VITE_API_URL="" +ARG VITE_SUPABASE_URL="" +ARG VITE_SUPABASE_ANON_KEY="" +ARG VITE_SENTRY_DSN="" + +ENV VITE_API_URL=$VITE_API_URL +ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL +ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY +ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSN + +RUN npm run build + +FROM python:3.11-slim + +ENV PORT=7860 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY VERSION VERSION +COPY backend/requirements.txt backend/requirements.txt +RUN pip install --no-cache-dir -r backend/requirements.txt + +COPY backend backend +COPY --from=frontend-build /app/frontend/dist frontend/dist + +WORKDIR /app/backend + +EXPOSE 7860 + +CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860}"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b356c7550ebaa397157c08efad78f7cfdfe4dc9e --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +--- +title: Aubm +sdk: docker +app_port: 7860 +license: mit +short_description: Automated Business Machines +--- + +# 🤖 Aubm + +### Enterprise-Grade AI Agent Orchestration & Collaboration Platform + +Aubm (Automated Unified Business Machines) is a sophisticated platform designed to orchestrate multiple autonomous AI agents to complete complex projects. Featuring **Human-in-the-Loop** supervision, **Dynamic DAG** task execution, and **Semantic RAG** context injection. + +--- + +## 🚀 Key Features + +- **Multi-Provider Support**: Seamless integration with OpenAI, AMD (inference.do-ai.run), Groq, Gemini, Qwen, Ollama, and OpenRouter. +- **Autonomous Orchestration**: Intelligent task prioritization and execution based on dependencies (DAG). +- **Human-in-the-Loop**: Approval-based workflow ensuring quality and safety. +- **Semantic Backpropagation**: Context from completed tasks is automatically injected into subsequent tasks. +- **Real-time Monitoring**: SSE-powered live logs and progress tracking. +- **Project Wizard**: AI-driven project creation and task decomposition. +- **Operational Safety**: Automatic recovery of stale runs and comprehensive health monitoring. + +--- + +## 🛠️ Tech Stack + +- **Frontend**: React + Vite + TypeScript (Styled with Vanilla CSS for maximum performance) +- **Backend**: FastAPI (Python 3.10+) +- **Database**: Supabase (Postgres + Auth + Real-time) +- **Deployment**: Optimized for Vercel (Serverless Backend + Static Frontend) + +--- + +## 🏗️ Project Structure + +```bash +aubm/ +├── backend/ # FastAPI Application & AI Core +│ ├── agents/ # LLM Provider Implementations +│ ├── routers/ # API Endpoints (Runner, Orchestrator) +│ ├── services/ # Business Logic (Queue, RAG, Guards) +│ └── main.py # App Entrypoint +├── frontend/ # React Application +│ ├── src/ # Components, Hooks, Context, Services +│ └── vite.config.ts # Vite Configuration +└── database/ # Supabase Schema & Migrations +``` + +--- + +## ⚙️ Getting Started + +### 1. Database Setup (Supabase) +1. Create a new project in [Supabase](https://supabase.com). +2. Go to the **SQL Editor** and execute the content of `backend/schema.sql`. +3. Enable **Auth** with your preferred providers (Email/Password by default). + +### 2. Backend Installation +```bash +cd backend +python -m venv venv +source venv/bin/activate # or venv\Scripts\activate on Windows +pip install -r requirements.txt +``` + +Create a `.env` file in `/backend`: +```env +SUPABASE_URL=your_project_url +SUPABASE_SERVICE_ROLE_KEY=your_service_role_key +OPENAI_API_KEY=optional_key +GROQ_API_KEY=optional_key +TAVILY_API_KEY=optional_key +# See SPEC.md for all available providers +``` + +Run the server: +```bash +uvicorn main:app --reload --port 8000 +``` + +### 3. Frontend Installation +```bash +cd frontend +npm install +``` + +Create a `.env` file in `/frontend`: +```env +VITE_API_URL=http://localhost:8000 +VITE_SUPABASE_URL=your_project_url +VITE_SUPABASE_ANON_KEY=your_anon_key +``` + +Run the development server: +```bash +npm run dev +``` + +--- + +## 📈 Operational Modes + +- **Embedded Worker**: Runs the task queue within the FastAPI process (set `TASK_QUEUE_EMBEDDED_WORKER=true`). +- **Standalone Worker**: For high-load environments, run the worker in a separate process: + ```bash + cd backend + python worker.py + ``` + +--- + +## Hugging Face Spaces + +This repository is ready to deploy as a Docker Space. Create a Hugging Face Space with SDK `Docker`, then push this repo to the Space remote. + +Configure these Space secrets or variables: + +```env +SUPABASE_URL=your_project_url +SUPABASE_SERVICE_ROLE_KEY=your_service_role_key +SUPABASE_ANON_KEY=your_anon_key +GROQ_API_KEY=optional_key +OPENAI_API_KEY=optional_key +GEMINI_API_KEY=optional_key +AMD_API_KEY=optional_key +TAVILY_API_KEY=optional_key +TASK_QUEUE_EMBEDDED_WORKER=true +``` + +`VITE_API_URL` can stay empty on Spaces because the frontend calls the FastAPI backend on the same origin. + +--- + +## 📄 Documentation + +For detailed technical architecture, refer to: +- [SPEC.md](./SPEC.md) - Deep technical specifications. +- [ROADMAP.md](./ROADMAP.md) - Future development goals. +- [docs/](./docs/) - Extended guides and manuals. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000000000000000000000000000000000000..1d712627526fa19fe1749c8213b10c5c7b5b4873 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,61 @@ +# Aubm Roadmap + +This document outlines the strategic evolution of Aubm, moving from a robust orchestration core to an enterprise-ready multi-agent operating layer. + +## Phase 1: Core Foundation (Completed) +- [x] Autonomous Agent Execution: Multi-provider support (OpenAI, Groq, Gemini, etc.). +- [x] Project Orchestration: Intelligent task scheduling and dependency management (DAG). +- [x] Human-in-the-Loop: Approval and rejection workflows for agent outputs. +- [x] Semantic RAG: Contextual memory injection across project tasks. +- [x] Real-time Logs: Streaming agent thoughts and actions via SSE. +- [x] Cost Control: Token-based budgeting and execution blocking. + +## Phase 2: Advanced Collaboration and Tools (Completed) +- [x] Multi-Agent Debates: Allow agents to cross-verify each other's outputs before human review. +- [x] Extended Toolbelt: + - [x] Web Browser Tool (via Playwright) for live data fetching. + - [x] Code Sandbox for executing and testing generated snippets. + - [x] File Generation (Excel, Word, and advanced PDF layouts). +- [x] Collaborative Editing: Real-time collaborative output refining for humans. +- [x] Mobile Experience: Capacitor-based mobile app for project monitoring (initialized). + +## Phase 3: Intelligence and Scale (Completed) +- [x] Fine-tuning Loop: Feedback loop (Like/Dislike) implemented for data collection. +- [x] Recursive Project Decomposition: Agents that can spawn sub-tasks and manage them. +- [x] Enterprise Security: + - [x] SSO Integration (Google, GitHub via Supabase). + - [x] Advanced RLS for granular team permissions. + - [x] Audit logs for every LLM interaction. +- [x] Agent Marketplace: Community-driven agent templates and specialized skill sets. + +## Phase 4: Autonomy and Beyond (Completed) +- [x] Self-Healing Infrastructure: Agents that can monitor health and apply safe patches. +- [x] Voice Interaction: Control navigation and hear project/task status updates via browser voice APIs. +- [x] VR/AR Dashboard: Spatial DAG viewer scaffold for layered project/task visualization. + +## Phase 5: Production Operations (Completed) +- [x] Operations Monitoring: Backend health summary endpoint and frontend monitoring dashboard with Supabase fallback. +- [x] Deployment Hardening: Dockerized backend/runtime profile and production CORS configuration. +- [x] Error Tracking: Sentry-compatible error reporting hooks for backend and frontend. +- [x] Performance Budgeting: Frontend code splitting and bundle-size targets. + +## Phase 6: Distributed Scale and Intelligence (In Progress) +- [x] Recursive Project Decomposition: Agents that can automatically break down goals. +- [x] Numerical Consistency (Semantic Backprop): Enforce absolute figures across tasks. +- [x] Visual Tooling: Integrated support for charts and AI illustrations. +- [x] Vercel Deployment: Monorepo serverless configuration. +- [x] Heuristic Output Guardrails: Prompt hardening, reviewer checks, and final-report filtering for placeholders, unsupported claims, and low-quality sections. +- [ ] Asynchronous Task Queue: Dedicated background workers (`worker.py`). +- [ ] Vectorized Long-term Memory: Cross-project semantic retrieval. +- [ ] Self-Optimizing Agents: Meta-prompting loops based on human feedback. + +## Phase 7: Structured Evidence and Entity Integrity (Next) +- [ ] Strict JSON Task Schemas: Enforce structured outputs per task type instead of free-form text. +- [ ] Mandatory `source_url` per Claim: Require evidence links for competitor, pricing, release, benchmark, and market claims. +- [ ] Entity Normalization Layer: Canonicalize entity names, merge aliases, and separate direct competitors from adjacent tools before final reporting. +- [ ] Semantic Deduplication: Collapse equivalent claims written differently across tasks. +- [ ] Evidence-Aware Final Report: Build the final report from normalized entities and validated claims only. + +--- + +*Last updated: May 6, 2026* diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000000000000000000000000000000000000..6b44ebc45b55c8bc3b9f8165e8c26af64b5d7dbf --- /dev/null +++ b/SPEC.md @@ -0,0 +1,137 @@ +# 🛠️ Aubm — Technical Specification + +> **Target Stack**: FastAPI (Python) + React/TypeScript (Vite) + Supabase (Postgres + Auth) + +This document provides a comprehensive technical blueprint for recreating Aubm. + +--- + +## 1. System Architecture + +Aubm follows a decoupled architecture with a centralized database (Supabase) acting as the source of truth and coordination layer. + +### Directory Structure +``` +aubm/ +├── backend/ # Python 3.10+ +│ ├── main.py # Application entrypoint & CRUD API +│ ├── worker.py # Standalone task queue worker +│ ├── schema.sql # Full DDL for Supabase +│ ├── agents/ # Provider-specific implementations +│ │ ├── base.py # Abstract BaseAgent class +│ │ ├── agent_factory.py # Factory for creating agent instances +│ │ └── {provider}_agent.py +│ ├── routers/ # Functional endpoint grouping +│ │ ├── agent_runner.py # Task execution logic +│ │ └── orchestrator.py # Multi-task project flow +│ └── services/ # Core business logic +│ ├── config.py # Configuration management +│ ├── task_queue.py # Background processing loop +│ └── semantic_backprop.py # RAG context builder +├── frontend/ # React + Vite + TS +│ ├── src/ +│ │ ├── components/ # UI Modular components +│ │ ├── services/ # API communication layer +│ │ ├── context/ # Auth & Global state +│ │ └── i18n/ # Multi-language support +│ └── vite.config.ts +└── database/ # Migrations & Seed data +``` + +--- + +## 2. Database Schema (Supabase/Postgres) + +### Core Tables + +| Table | Purpose | Key Columns | +|-------|---------|-------------| +| `profiles` | User extensions | `id (uuid)`, `role`, `full_name`, `avatar_url` | +| `projects` | Project containers | `id`, `name`, `description`, `context`, `owner_id`, `status` | +| `agents` | AI Identities | `id`, `name`, `role`, `api_provider`, `model`, `system_prompt` | +| `tasks` | Units of work | `id`, `project_id`, `assigned_agent_id`, `status`, `output_data` | +| `task_runs` | Execution history | `id`, `task_id`, `agent_id`, `status`, `error_message` | +| `agent_logs` | Execution traces | `id`, `task_id`, `action`, `content`, `metadata` | +| `app_config` | Global settings | `key`, `value` (JSONB) | + +### Status Enums +- **Tasks**: `todo`, `in_progress`, `awaiting_approval`, `done`, `failed`, `cancelled`. +- **Task Runs**: `queued`, `running`, `completed`, `failed`, `cancelled`. +- **Profiles**: `user`, `manager`, `admin`. + +--- + +## 3. Backend Logic + +### Agent Execution Flow +1. **Request**: `POST /tasks/{id}/run` +2. **Initialization**: Fetch task, agent, and project data. +3. **Context Building**: `semantic_backprop` fetches outputs from previous tasks in the same project. +4. **Agent Factory**: Instantiates the correct `BaseAgent` subclass (e.g., `GroqAgent`). +5. **Execution**: + - LLM call with dynamic prompt. + - Real-time logging to `agent_logs` via SSE. +6. **Guardrails**: + - `output_cleaner`: Strips markdown artifacts. + - `language_guard`: Ensures output matches `app_config["output_language"]`. +7. **Persistence**: Updates `task.output_data` and sets status to `awaiting_approval`. + +### Orchestration Engine +- Processes a project's task list as a Directed Acyclic Graph (DAG). +- Respects `is_critical` and `priority` fields. +- Auto-assigns available agents from the `agents` pool if no agent is pre-assigned. + +### Tool System (Phase 2) +- **Tool Registry**: A central registry where tools are defined and permissioned. +- **Browser Tool**: Uses Playwright for headless browsing and content extraction. +- **Sandbox Tool**: Executes code in a restricted environment. +- **Integration**: Tools are exposed to agents via the OpenAI function-calling/tool-calling schema. + +--- + +## 4. Frontend Design System + +- **Styling**: Vanilla CSS with modern variables (HSL colors, glassmorphism). +- **Icons**: Lucide React. +- **State Management**: React Context + Hooks. +- **Features**: + - Kanban Board for task management. + - Real-time streaming console for agent thoughts. + - Interactive Project Wizard for quick setup. + - Analytics dashboard for project performance. + +--- + +## 5. Deployment Guide + +### Vercel Integration +The project is designed to run seamlessly on Vercel: +- **Frontend**: Standard Vite build. +- **Backend**: Python Serverless Functions. +- **Database**: External Supabase instance. + +### Local Setup +1. **DB**: Apply `schema.sql` to Supabase. +2. **Backend**: `pip install -r requirements.txt` & `uvicorn main:app`. +3. **Frontend**: `npm install` & `npm run dev`. + +--- + +## 6. Key Dependencies + +### Backend +- `fastapi`, `supabase`, `openai`, `groq`, `google-genai`, `playwright`, `folium`. + +### Frontend +- `react`, `lucide-react`, `framer-motion` (for animations), `i18next`. + +--- + +## 7. Security (RLS) +- **Projects**: Only visible to owner or if `is_public=true`. +- **Config**: Only writable by users with `role='admin'`. +- **Agents**: Writable by `manager` or `admin`. +- **Tasks**: Protected by project-level RLS. + +--- +*End of Specification* diff --git a/VERSION b/VERSION new file mode 100644 index 0000000000000000000000000000000000000000..faef31a4357c48d6e4c55e84c8be8e3bc9055e20 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.7.0 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..c8e518e9cd2c66b8047b30961293c2d56158551f --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,19 @@ +# Supabase Configuration +SUPABASE_URL=https://your-project-id.supabase.co +SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here + +# AI Provider Keys +OPENAI_API_KEY=your-openai-key +GROQ_API_KEY=your-groq-key +GEMINI_API_KEY=your-gemini-key +ANTHROPIC_API_KEY=your-anthropic-key +TAVILY_API_KEY=your-tavily-key + +# App Settings +PORT=8000 +ALLOWED_ORIGINS=http://localhost:5173,https://your-app.vercel.app +TASK_QUEUE_EMBEDDED_WORKER=true +OUTPUT_LANGUAGE=en + +# Error Tracking +SENTRY_DSN=your-sentry-dsn diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0afd6ed339be5637341ba3fce2a238d7e5ad0bd3 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,32 @@ +# Build stage for backend +FROM python:3.11-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Set work directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Install Playwright browsers and their dependencies +RUN playwright install --with-deps chromium + +# Copy project +COPY . . + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/agents/agent_factory.py b/backend/agents/agent_factory.py new file mode 100644 index 0000000000000000000000000000000000000000..6bf355ec48a8ba77a4cd0e6592940b5fa8e7153c --- /dev/null +++ b/backend/agents/agent_factory.py @@ -0,0 +1,44 @@ +from typing import Dict, Type +from .base import BaseAgent +from .openai_agent import OpenAIAgent +from .amd_agent import AMDAgent +from .groq_agent import GroqAgent +from .gemini_agent import GeminiAgent +from .local_agent import LocalAgent +from services.config import settings + +# Map of providers to their respective classes +PROVIDER_MAP: Dict[str, Type[BaseAgent]] = { + "openai": OpenAIAgent, + "amd": AMDAgent, + "groq": GroqAgent, + "gemini": GeminiAgent, + "local": LocalAgent, + "ollama": LocalAgent +} + +class AgentFactory: + @staticmethod + def get_agent(provider: str, name: str, role: str, model: str, system_prompt: str = None) -> BaseAgent: + """ + Instantiates the appropriate agent based on the provider string. + Includes a fallback to Groq if OpenAI is requested but no key is provided. + """ + provider = provider.lower() + + # Groq Redirection Logic + if provider == "openai" and not settings.OPENAI_API_KEY: + # Check if we have a Groq key before redirecting + if settings.GROQ_API_KEY: + provider = "groq" + model = "llama-3.3-70b-versatile" # Robust fallback model + else: + # If neither is available, let it fail with the original provider + pass + + agent_class = PROVIDER_MAP.get(provider) + + if not agent_class: + raise ValueError(f"Unsupported agent provider: {provider}") + + return agent_class(name=name, role=role, model=model, system_prompt=system_prompt) diff --git a/backend/agents/amd_agent.py b/backend/agents/amd_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..2981dc6aa0bc03aa593e1ce0eb95870d12228e83 --- /dev/null +++ b/backend/agents/amd_agent.py @@ -0,0 +1,33 @@ +from .base import BaseAgent +from typing import Dict, Any, List +import openai +from services.config import settings, config_service + +class AMDAgent(BaseAgent): + """ + Agent implementation for AMD Inference (inference.do-ai.run). + Compatible with OpenAI's API format. + """ + def __init__(self, name: str, role: str, model: str = "gpt-4o", system_prompt: str = None): + super().__init__(name, role, model, system_prompt) + + self.provider_config = config_service.get_provider_config("amd") + api_key = self.provider_config.get("api_key") or settings.AMD_API_KEY + + self.client = openai.AsyncOpenAI( + api_key=api_key, + base_url=self.provider_config.get("base_url", "https://inference.do-ai.run/v1") + ) + self.temperature = self.provider_config.get("temperature", 0.7) + self.max_tokens = self.provider_config.get("max_tokens", 4096) + + async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]: + return await self._run_openai_compatible( + provider="amd", + create_fn=self.client.chat.completions.create, + task_description=task_description, + context=context, + use_tools=use_tools, + extra_context=extra_context, + response_format={"type": "json_object"} + ) diff --git a/backend/agents/base.py b/backend/agents/base.py new file mode 100644 index 0000000000000000000000000000000000000000..d183c0e002ef87bf9e06d89fa59e5857415914b8 --- /dev/null +++ b/backend/agents/base.py @@ -0,0 +1,162 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any, List, Optional +import json + +class BaseAgent(ABC): + def __init__(self, name: str, role: str, model: str, system_prompt: Optional[str] = None): + self.name = name + self.role = role + self.model = model + self.system_prompt = system_prompt or f"You are {name}, acting as a {role}." + + @abstractmethod + async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]: + """ + Executes a task given its description and previous context. + Returns a dictionary containing the output data. + """ + pass + + def _format_context(self, context: List[Dict[str, Any]]) -> str: + """Helper to format previous task outputs for the current agent.""" + if not context: + return "No previous context available." + + formatted = "Previous tasks context:\n" + for item in context: + formatted += f"- Task: {item.get('title')}\n Output: {json.dumps(item.get('output_data', {}))}\n" + return formatted + + def _build_json_prompt(self, task_description: str, context: List[Dict[str, Any]], extra_context: str = "") -> str: + return f""" +Task: {task_description} + +{self._format_context(context)} + +{extra_context} + +Please provide your output as a JSON object. +""" + + def _build_chat_messages(self, task_description: str, context: List[Dict[str, Any]], extra_context: str = "") -> List[Dict[str, Any]]: + return [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": self._build_json_prompt(task_description, context, extra_context)} + ] + + def _parse_json_output(self, content: str) -> Any: + """Parse strict JSON first, then tolerate fenced or prefixed JSON.""" + if not content: + return {} + + try: + return json.loads(content) + except json.JSONDecodeError: + pass + + try: + if "```json" in content: + clean = content.split("```json", 1)[1].split("```", 1)[0].strip() + elif "```" in content: + clean = content.split("```", 1)[1].split("```", 1)[0].strip() + else: + object_start, array_start = content.find("{"), content.find("[") + starts = [index for index in (object_start, array_start) if index != -1] + start = min(starts) if starts else -1 + if start == array_start: + end = content.rfind("]") + else: + end = content.rfind("}") + clean = content[start:end + 1] if start != -1 and end != -1 else content + return json.loads(clean) + except Exception: + return {"raw_text": content} + + def _parse_tool_arguments(self, arguments: str | None) -> Dict[str, Any]: + parsed = self._parse_json_output(arguments or "{}") + return parsed if isinstance(parsed, dict) else {} + + async def _append_tool_results(self, messages: List[Dict[str, Any]], tool_calls: Any, tool_registry: Any) -> None: + for tool_call in tool_calls or []: + tool_name = tool_call.function.name + tool_args = self._parse_tool_arguments(tool_call.function.arguments) + tool_result = await tool_registry.call_tool(tool_name, tool_args) + + messages.append({ + "tool_call_id": tool_call.id, + "role": "tool", + "name": tool_name, + "content": str(tool_result), + }) + + async def _run_openai_compatible( + self, + provider: str, + create_fn, + task_description: str, + context: List[Dict[str, Any]], + use_tools: bool = False, + extra_context: str = "", + **extra_kwargs + ) -> Dict[str, Any]: + """ + Unified runner for OpenAI-compatible APIs (OpenAI, Groq, etc.) + """ + from tools.registry import tool_registry + + messages = self._build_chat_messages(task_description, context, extra_context) + + is_reasoning_model = "gpt-oss-" in self.model or self.model.startswith("o1-") or self.model.startswith("o3-") + + kwargs = { + "model": self.model, + "messages": messages, + **extra_kwargs + } + + # Handle temperature/max_tokens based on model type + if is_reasoning_model: + # Reasoning models prefer temperature 1.0 or none + kwargs["temperature"] = extra_kwargs.get("temperature", 1.0) + # Use max_completion_tokens if provided, otherwise default to max_tokens logic but renamed + if "max_completion_tokens" not in kwargs: + kwargs["max_completion_tokens"] = getattr(self, "max_tokens", 4096) + # Standard max_tokens is often forbidden in reasoning models + kwargs.pop("max_tokens", None) + else: + kwargs["temperature"] = getattr(self, "temperature", 0.7) + kwargs["max_tokens"] = getattr(self, "max_tokens", 4096) + + if use_tools: + # Note: Many reasoning models don't support tools yet, but we'll include if requested + kwargs["tools"] = tool_registry.get_tool_definitions() + kwargs["tool_choice"] = "auto" + + response = await create_fn(**kwargs) + message = response.choices[0].message + + # Handle tool calls + if message.tool_calls: + messages.append(message) + await self._append_tool_results(messages, message.tool_calls, tool_registry) + + # Second call after tool execution + # Remove tools from second call to force a final answer + kwargs.pop("tools", None) + kwargs.pop("tool_choice", None) + + final_response = await create_fn(**kwargs) + content = final_response.choices[0].message.content + else: + content = message.content + + return self._result(provider, content or "") + + def _result(self, provider: str, content: str) -> Dict[str, Any]: + return { + "agent_name": self.name, + "provider": provider, + "model": self.model, + "raw_output": content, + "data": self._parse_json_output(content) + } diff --git a/backend/agents/gemini_agent.py b/backend/agents/gemini_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..8e31ced4c45277abac12a09edf57e11c601d7268 --- /dev/null +++ b/backend/agents/gemini_agent.py @@ -0,0 +1,37 @@ +from .base import BaseAgent +from typing import Dict, Any, List +from google import genai +from services.config import settings, config_service + +class GeminiAgent(BaseAgent): + """ + Agent implementation for Google Gemini using the new google-genai SDK. + """ + def __init__(self, name: str, role: str, model: str = "gemini-2.0-flash", system_prompt: str = None): + super().__init__(name, role, model, system_prompt) + + # Load dynamic config + self.provider_config = config_service.get_provider_config("gemini") + api_key = self.provider_config.get("api_key") or settings.GEMINI_API_KEY + + self.client = genai.Client(api_key=api_key) + self.temperature = self.provider_config.get("temperature", 0.7) + + async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]: + full_prompt = f""" +System Instruction: {self.system_prompt} + +{self._build_json_prompt(task_description, context, extra_context)} +""" + + # Gemini 2.0 Flash is very fast. + response = await self.client.aio.models.generate( + model=self.model, + contents=full_prompt, + config={ + "temperature": self.temperature, + "response_mime_type": "application/json", + } + ) + + return self._result("gemini", response.text or "") diff --git a/backend/agents/groq_agent.py b/backend/agents/groq_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..c8b72bb8508eef97deb09ae431d9816ad07aab44 --- /dev/null +++ b/backend/agents/groq_agent.py @@ -0,0 +1,98 @@ +import logging +from .base import BaseAgent +from typing import Dict, Any, List +import groq +import json +from services.config import settings, config_service +from tools.registry import tool_registry + +logger = logging.getLogger("uvicorn") + +GROQ_ROTATION_POOL = [ + "llama-3.3-70b-versatile", + "openai/gpt-oss-120b", + "meta-llama/llama-4-scout-17b-16e-instruct", + "qwen/qwen3-32b", + "openai/gpt-oss-20b", + "groq/compound", + "llama-3.1-8b-instant" +] + +class GroqAgent(BaseAgent): + """ + Agent implementation for Groq with automatic model rotation for rate limits. + """ + def __init__(self, name: str, role: str, model: str = "llama-3.3-70b-versatile", system_prompt: str = None): + # Auto-migrate decommissioned models + if "llama-3.1-70b" in model: + model = "llama-3.3-70b-versatile" + + super().__init__(name, role, model, system_prompt) + + # Load dynamic config + self.provider_config = config_service.get_provider_config("groq") + api_key = self.provider_config.get("api_key") or settings.GROQ_API_KEY + + self.client = groq.AsyncGroq(api_key=api_key) + self.temperature = self.provider_config.get("temperature", 0.7) + self.max_tokens = self.provider_config.get("max_tokens", 4096) + self.reasoning_effort = self.provider_config.get("reasoning_effort", "medium") + + def _format_context(self, context: List[Dict[str, Any]]) -> str: + """Extremely aggressive truncation for Groq TPM limits.""" + if not context: + return "No previous context available." + + # Only take the last 3 tasks to save tokens + recent_context = context[-3:] + + formatted = "Previous tasks context (EXTREMELY TRUNCATED for Groq):\n" + for item in recent_context: + output_raw = json.dumps(item.get('output_data', {})) + # 800 chars is roughly 200 tokens. + if len(output_raw) > 800: + output_raw = output_raw[:800] + "... [TRUNCATED]" + + formatted += f"- Task: {item.get('title')}\n Output: {output_raw}\n" + return formatted + + async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]: + # Very limited semantic context + if len(extra_context) > 1000: + extra_context = extra_context[:1000] + "... [TRUNCATED]" + + try: + return await self._execute_run(task_description, context, use_tools, extra_context) + except groq.RateLimitError as e: + logger.warning(f"Rate limit reached for {self.model} (429). Attempting model rotation...") + + # Find current model index in pool + try: + current_idx = GROQ_ROTATION_POOL.index(self.model) + except ValueError: + current_idx = -1 + + # Try the next model in the pool + next_idx = (current_idx + 1) % len(GROQ_ROTATION_POOL) + fallback_model = GROQ_ROTATION_POOL[next_idx] + + logger.info(f"Rotating from {self.model} to {fallback_model}") + self.model = fallback_model + + # Retry once with fallback model + return await self._execute_run(task_description, context, use_tools, extra_context) + + async def _execute_run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]: + extra_kwargs = {} + if "gpt-oss-" in self.model: + extra_kwargs["reasoning_effort"] = self.reasoning_effort + + return await self._run_openai_compatible( + provider="groq", + create_fn=self.client.chat.completions.create, + task_description=task_description, + context=context, + use_tools=use_tools, + extra_context=extra_context, + **extra_kwargs + ) diff --git a/backend/agents/local_agent.py b/backend/agents/local_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..008e5fb63ad5715ba21d4d90e760013000ae0076 --- /dev/null +++ b/backend/agents/local_agent.py @@ -0,0 +1,48 @@ +from .base import BaseAgent +from typing import Dict, Any, List +import httpx +from services.config import config_service + +class LocalAgent(BaseAgent): + """ + Agent implementation for Local LLMs (Ollama). + """ + def __init__(self, name: str, role: str, model: str = "llama3.1:8b", system_prompt: str = None): + super().__init__(name, role, model, system_prompt) + + # Load dynamic config + self.provider_config = config_service.get_provider_config("ollama") + self.base_url = self.provider_config.get("base_url", "http://localhost:11434") + self.temperature = self.provider_config.get("temperature", 0.7) + + async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]: + full_prompt = f""" +System Instructions: {self.system_prompt} + +{self._build_json_prompt(task_description, context, extra_context)} +""" + + async with httpx.AsyncClient(timeout=60.0) as client: + try: + response = await client.post( + f"{self.base_url}/api/generate", + json={ + "model": self.model, + "prompt": full_prompt, + "stream": False, + "format": "json", + "options": { + "temperature": self.temperature + } + } + ) + response.raise_for_status() + result = response.json() + return self._result("local", result.get("response", "{}")) + except Exception as e: + return { + "agent_name": self.name, + "provider": "local", + "status": "error", + "error": f"Ollama connection failed: {str(e)}" + } diff --git a/backend/agents/openai_agent.py b/backend/agents/openai_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..971614226194a1cf7d58db553ff72c0981e6e42b --- /dev/null +++ b/backend/agents/openai_agent.py @@ -0,0 +1,28 @@ +from .base import BaseAgent +from typing import Dict, Any, List +import openai +from services.config import settings, config_service +from tools.registry import tool_registry + +class OpenAIAgent(BaseAgent): + def __init__(self, name: str, role: str, model: str = "gpt-4o", system_prompt: str = None): + super().__init__(name, role, model, system_prompt) + + # Load dynamic config + self.provider_config = config_service.get_provider_config("openai") + api_key = self.provider_config.get("api_key") or settings.OPENAI_API_KEY + + self.client = openai.AsyncOpenAI(api_key=api_key) + self.temperature = self.provider_config.get("temperature", 0.7) + self.max_tokens = self.provider_config.get("max_tokens", 4096) + + async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]: + return await self._run_openai_compatible( + provider="openai", + create_fn=self.client.chat.completions.create, + task_description=task_description, + context=context, + use_tools=use_tools, + extra_context=extra_context, + response_format={"type": "json_object"} + ) diff --git a/backend/agents_debug.json b/backend/agents_debug.json new file mode 100644 index 0000000000000000000000000000000000000000..25bd52fe9a7facac6c9a603087bdc24abbf85788 --- /dev/null +++ b/backend/agents_debug.json @@ -0,0 +1 @@ +[{"id": "297ef087-89af-4e3b-8d80-c5c5c7499e3b", "name": "GPT-4o", "role": "General Intelligence", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You are a highly capable AI assistant.", "created_at": "2026-05-04T14:32:03.072888+00:00", "updated_at": "2026-05-04T14:32:03.072888+00:00", "user_id": null}, {"id": "64aebf0c-625a-4c6f-895d-a36274c4f9fd", "name": "AMD-4o", "role": "Performance Specialist", "api_provider": "amd", "model": "gpt-4o", "system_prompt": "You are a high-performance agent running on AMD infrastructure.", "created_at": "2026-05-04T14:32:03.072888+00:00", "updated_at": "2026-05-04T14:32:03.072888+00:00", "user_id": null}, {"id": "f7cc5a82-1e7d-4f21-9855-922fb82cd6f9", "name": "Llama-3-70B", "role": "Fast Logic", "api_provider": "groq", "model": "llama3-70b-8192", "system_prompt": "You are a fast and efficient reasoning agent.", "created_at": "2026-05-04T14:32:03.072888+00:00", "updated_at": "2026-05-04T14:32:03.072888+00:00", "user_id": null}, {"id": "1d988b3c-38c1-4132-85e1-82e7a4bc4f8a", "name": "Growth Hacker", "role": "Marketing Expert", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You are a Growth Hacker focused on low-cost, high-impact strategies.", "created_at": "2026-05-04T15:50:52.628388+00:00", "updated_at": "2026-05-04T15:50:52.628388+00:00", "user_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578"}, {"id": "7b056c90-f4a6-4629-81b1-f4316b76d091", "name": "Growth Hacker", "role": "Marketing Expert", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You are a Growth Hacker focused on low-cost, high-impact strategies.", "created_at": "2026-05-04T16:22:10.993123+00:00", "updated_at": "2026-05-04T16:22:10.993123+00:00", "user_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578"}, {"id": "138d7d29-b2c0-4ffb-b89f-1a0965dca6b6", "name": "Planner", "role": "Project Planner", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You decompose goals into clear, ordered implementation tasks.", "created_at": "2026-05-04T18:06:05.280562+00:00", "updated_at": "2026-05-04T18:06:05.280562+00:00", "user_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578"}, {"id": "edfc99cf-a70c-42e0-a2f6-4508bb6aac33", "name": "Builder", "role": "Implementation Agent", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You implement practical, production-oriented solutions with concise output.", "created_at": "2026-05-04T18:06:05.280562+00:00", "updated_at": "2026-05-04T18:06:05.280562+00:00", "user_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578"}, {"id": "5ef6c6f3-fc4a-40ba-abe8-7630fefcece2", "name": "Reviewer", "role": "Quality Reviewer", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You review outputs for correctness, security, completeness, and missing tests.", "created_at": "2026-05-04T18:06:05.280562+00:00", "updated_at": "2026-05-04T18:06:05.280562+00:00", "user_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578"}] \ No newline at end of file diff --git a/backend/api/index.py b/backend/api/index.py new file mode 100644 index 0000000000000000000000000000000000000000..bbbc533e8f7e4542abbb527bfbbf2c4b195cffb9 --- /dev/null +++ b/backend/api/index.py @@ -0,0 +1 @@ +from main import app diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000000000000000000000000000000000000..b559fe28fcb2ce7a666f34557d4f4f50bd66fded --- /dev/null +++ b/backend/main.py @@ -0,0 +1,102 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, Response +import os +import json +from pathlib import Path +from dotenv import load_dotenv +import sentry_sdk + + +def _load_app_version() -> str: + version_file = Path(__file__).resolve().parent.parent / "VERSION" + if version_file.exists(): + value = version_file.read_text(encoding="utf-8").strip() + if value: + return value + return os.getenv("APP_VERSION", "0.7.0") + + +# Load environment variables +load_dotenv() +FRONTEND_DIST = Path(__file__).resolve().parent.parent / "frontend" / "dist" +APP_VERSION = _load_app_version() + +# Sentry Initialization +SENTRY_DSN = os.getenv("SENTRY_DSN") +if SENTRY_DSN: + sentry_sdk.init( + dsn=SENTRY_DSN, + traces_sample_rate=1.0, + profiles_sample_rate=1.0, + ) + +app = FastAPI( + title="Aubm API", + description="Enterprise-Grade AI Agent Orchestration & Collaboration Platform", + version=APP_VERSION +) + +# CORS Configuration +allowed_origins = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173").split(",") +app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins if allowed_origins != ["*"] else ["*"], + allow_origin_regex=os.getenv("ALLOWED_ORIGIN_REGEX"), + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.get("/") +async def root(): + index_path = FRONTEND_DIST / "index.html" + if index_path.exists(): + return FileResponse(index_path) + + return { + "status": "online", + "message": "Aubm API is operational", + "version": APP_VERSION + } + +# Placeholder for routers +from routers import agent_runner, orchestrator, monitoring + +app.include_router(agent_runner.router, prefix="/tasks", tags=["Tasks"]) +app.include_router(orchestrator.router, prefix="/orchestrator", tags=["Orchestration"]) +app.include_router(monitoring.router, prefix="/monitoring", tags=["Monitoring"]) + +@app.get("/runtime-config.js", include_in_schema=False) +async def runtime_config(): + config = { + "apiUrl": os.getenv("VITE_API_URL", ""), + "supabaseUrl": os.getenv("VITE_SUPABASE_URL", os.getenv("SUPABASE_URL", "")), + "supabaseAnonKey": os.getenv("VITE_SUPABASE_ANON_KEY", os.getenv("SUPABASE_ANON_KEY", "")), + "sentryDsn": os.getenv("VITE_SENTRY_DSN", os.getenv("SENTRY_DSN", "")), + "appVersion": APP_VERSION, + } + return Response( + content=f"window.__AUBM_CONFIG__ = {json.dumps(config)};", + media_type="application/javascript", + ) + +@app.get("/{path:path}", include_in_schema=False) +async def serve_frontend(path: str): + if not FRONTEND_DIST.exists(): + return await root() + + requested_path = FRONTEND_DIST / path + if requested_path.is_file(): + return FileResponse(requested_path) + + index_path = FRONTEND_DIST / "index.html" + if index_path.exists(): + return FileResponse(index_path) + + return await root() + +if __name__ == "__main__": + import uvicorn + from services.config import settings + uvicorn.run("main:app", host="0.0.0.0", port=settings.PORT, reload=True) diff --git a/backend/project_debug.json b/backend/project_debug.json new file mode 100644 index 0000000000000000000000000000000000000000..a4216b8a2c9066f0de8302f3acef633c030839fe --- /dev/null +++ b/backend/project_debug.json @@ -0,0 +1 @@ +{"id": "53f39562-09aa-447b-b251-3844e1415d4c", "name": "Commercial analysis", "description": "I want to evaluate differents applications similar to this app", "context": "search in internet", "owner_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578", "status": "active", "is_public": true, "created_at": "2026-05-04T16:38:03.872534+00:00", "updated_at": "2026-05-04T16:38:03.872534+00:00", "team_id": null} \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..d272a518ec8a407910636cf227dbedd58d0cd306 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,19 @@ +fastapi +uvicorn[standard] +supabase +openai +groq +google-genai +playwright +folium +python-dotenv +pydantic +pydantic-settings +httpx +jinja2 +python-multipart +reportlab +pandas +openpyxl +psutil +sentry-sdk[fastapi] diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..873f7bbbe945dde0325ebbbd742d46dd672befa0 --- /dev/null +++ b/backend/routers/__init__.py @@ -0,0 +1 @@ +# Routers package diff --git a/backend/routers/agent_runner.py b/backend/routers/agent_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..915334b3ebbc01dc09252eb26bb8fa8205847d7f --- /dev/null +++ b/backend/routers/agent_runner.py @@ -0,0 +1,151 @@ +from fastapi import APIRouter, HTTPException, BackgroundTasks +from services.supabase_service import supabase +from services.agent_runner_service import AgentRunnerService +from services.output_quality import report_text_from_output +import logging + +router = APIRouter() +logger = logging.getLogger("uvicorn") + + +def _assert_task_quality(task: dict): + output_data = task.get("output_data") or {} + if not isinstance(output_data, dict): + raise HTTPException(status_code=400, detail="Task output is missing or malformed.") + if output_data.get("error"): + raise HTTPException(status_code=400, detail=f"Task execution failed: {output_data['error']}") + rendered = report_text_from_output(output_data).strip() + if not rendered or rendered in ("{}", "[]"): + raise HTTPException(status_code=400, detail="Task has no usable output to approve.") + quality_review = output_data.get("quality_review") + if not quality_review: + raise HTTPException(status_code=400, detail="Task output is missing quality validation.") + if quality_review.get("approved"): + return + reasons = quality_review.get("fail_reasons") or ["Task output failed quality validation."] + raise HTTPException(status_code=400, detail=f"Task output failed quality review: {'; '.join(reasons)}") + +def update_task_status(task_id: str, status: str): + result = ( + supabase.table("tasks") + .update({"status": status}) + .eq("id", task_id) + .execute() + ) + if not result.data: + raise HTTPException(status_code=404, detail="Task not found or status was not updated") + + task_data = result.data[0] + + project_id = task_data.get("project_id") + if project_id: + task_result = ( + supabase.table("tasks") + .select("id,status") + .eq("project_id", project_id) + .execute() + ) + tasks = task_result.data or [] + if status == "done" and tasks and all(task.get("status") == "done" for task in tasks): + supabase.table("projects").update({"status": "completed"}).eq("id", project_id).execute() + elif status != "done": + supabase.table("projects").update({"status": "active"}).eq("id", project_id).execute() + + return task_data + +@router.post("/{task_id}/run") +async def run_task(task_id: str, background_tasks: BackgroundTasks): + """ + Triggers the execution of a specific task. + """ + # 1. Fetch task data + task_res = supabase.table("tasks").select("*, project:projects(*)").eq("id", task_id).single().execute() + if not task_res.data: + raise HTTPException(status_code=404, detail="Task not found") + + task = task_res.data + + # 2. Check if agent is assigned + agent_id = task.get("assigned_agent_id") + if not agent_id: + raise HTTPException(status_code=400, detail="No agent assigned to this task") + + # 3. Fetch agent data + agent_res = supabase.table("agents").select("*").eq("id", agent_id).single().execute() + if not agent_res.data: + raise HTTPException(status_code=404, detail="Assigned agent not found") + + agent_data = agent_res.data + + # 4. Update task status to in_progress + supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute() + + # 5. Run in background + background_tasks.add_task(AgentRunnerService.execute_agent_logic, task, agent_data) + + return {"message": "Task execution started", "task_id": task_id} + +@router.post("/{task_id}/approve") +async def approve_task(task_id: str): + task_res = supabase.table("tasks").select("*").eq("id", task_id).single().execute() + if not task_res.data: + raise HTTPException(status_code=404, detail="Task not found") + _assert_task_quality(task_res.data) + task = update_task_status(task_id, "done") + return {"message": "Task approved", "task": task} + +@router.post("/{task_id}/reject") +async def reject_task(task_id: str): + task = update_task_status(task_id, "todo") + return {"message": "Task rejected", "task": task} +@router.post("/project/{project_id}/approve-all") +async def approve_all_tasks(project_id: str): + """ + Approves all tasks in a project that are awaiting approval. + """ + waiting_tasks = ( + supabase.table("tasks") + .select("*") + .eq("project_id", project_id) + .eq("status", "awaiting_approval") + .execute() + .data + or [] + ) + blocked = [] + approvable_ids = [] + for task in waiting_tasks: + try: + _assert_task_quality(task) + approvable_ids.append(task["id"]) + except HTTPException: + blocked.append(task["title"]) + + # 1. Update tasks + result_data = [] + if approvable_ids: + result = ( + supabase.table("tasks") + .update({"status": "done"}) + .eq("project_id", project_id) + .in_("id", approvable_ids) + .execute() + ) + result_data = result.data or [] + + # 2. Check if all tasks in project are now done + task_result = ( + supabase.table("tasks") + .select("status") + .eq("project_id", project_id) + .execute() + ) + tasks = task_result.data or [] + if tasks and all(task.get("status") == "done" for task in tasks): + supabase.table("projects").update({"status": "completed"}).eq("id", project_id).execute() + + return { + "message": f"Approved {len(result_data)} tasks", + "count": len(result_data), + "blocked": blocked + } diff --git a/backend/routers/monitoring.py b/backend/routers/monitoring.py new file mode 100644 index 0000000000000000000000000000000000000000..0fcbdb77e314eae3838af4211f7baf388ba20118 --- /dev/null +++ b/backend/routers/monitoring.py @@ -0,0 +1,70 @@ +from datetime import datetime, timezone +from fastapi import APIRouter +from services.supabase_service import supabase + +router = APIRouter() + + +def _count_table(table_name: str) -> int: + response = supabase.table(table_name).select("id", count="exact").limit(1).execute() + return response.count or 0 + + +@router.get("/summary") +async def monitoring_summary(): + """ + Lightweight operational summary for dashboards and uptime checks. + """ + checks = { + "api": "ok", + "database": "ok", + } + + counts = { + "projects": 0, + "tasks": 0, + "agents": 0, + "task_runs": 0, + "failed_tasks": 0, + "pending_reviews": 0, + } + + try: + counts["projects"] = _count_table("projects") + counts["tasks"] = _count_table("tasks") + counts["agents"] = _count_table("agents") + counts["task_runs"] = _count_table("task_runs") + counts["failed_tasks"] = ( + supabase.table("tasks") + .select("id", count="exact") + .eq("status", "failed") + .limit(1) + .execute() + .count + or 0 + ) + counts["pending_reviews"] = ( + supabase.table("tasks") + .select("id", count="exact") + .eq("status", "awaiting_approval") + .limit(1) + .execute() + .count + or 0 + ) + except Exception as exc: + checks["database"] = "error" + return { + "status": "degraded", + "checks": checks, + "counts": counts, + "error": str(exc), + "timestamp": datetime.now(timezone.utc).isoformat(), + } + + return { + "status": "ok", + "checks": checks, + "counts": counts, + "timestamp": datetime.now(timezone.utc).isoformat(), + } diff --git a/backend/routers/orchestrator.py b/backend/routers/orchestrator.py new file mode 100644 index 0000000000000000000000000000000000000000..fd14bbb2aeb034e73716af559696d80ae13f3be4 --- /dev/null +++ b/backend/routers/orchestrator.py @@ -0,0 +1,169 @@ +from fastapi import APIRouter, BackgroundTasks, HTTPException +from fastapi.responses import Response +from services.orchestrator_service import orchestrator_service +from pydantic import BaseModel +from io import BytesIO +from reportlab.lib.pagesizes import letter +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle +from reportlab.graphics.shapes import Drawing +from reportlab.graphics.charts.barcharts import VerticalBarChart +from reportlab.graphics.charts.piecharts import Pie +from reportlab.lib import colors +from reportlab.lib.units import inch +import re + +router = APIRouter() + +def _safe_filename(value: str) -> str: + return re.sub(r"[^a-zA-Z0-9_-]+", "_", value).strip("_").lower() or "report" + +def _bar_chart(title: str, rows: list[dict]) -> Drawing: + drawing = Drawing(460, 180) + chart = VerticalBarChart() + chart.x = 40 + chart.y = 35 + chart.height = 110 + chart.width = 380 + chart.data = [[row["value"] for row in rows]] + chart.categoryAxis.categoryNames = [row["label"] for row in rows] + chart.valueAxis.valueMin = 0 + chart.valueAxis.valueMax = max([row["value"] for row in rows] + [1]) + chart.valueAxis.valueStep = max(1, round(chart.valueAxis.valueMax / 4)) + chart.bars[0].fillColor = colors.HexColor("#14b8a6") + drawing.add(chart) + return drawing + +def _pie_chart(rows: list[dict]) -> Drawing: + drawing = Drawing(460, 180) + pie = Pie() + pie.x = 150 + pie.y = 20 + pie.width = 140 + pie.height = 140 + pie.data = [row["value"] for row in rows] + pie.labels = [row["label"] for row in rows] + pie.slices.strokeWidth = 0.5 + palette = [colors.HexColor("#22c55e"), colors.HexColor("#facc15"), colors.HexColor("#ef4444")] + for index, color in enumerate(palette[:len(rows)]): + pie.slices[index].fillColor = color + drawing.add(pie) + return drawing + +def _report_pdf_bytes(title: str, content: str, charts: dict | None = None) -> bytes: + buffer = BytesIO() + doc = SimpleDocTemplate( + buffer, + pagesize=letter, + rightMargin=0.7 * inch, + leftMargin=0.7 * inch, + topMargin=0.7 * inch, + bottomMargin=0.7 * inch, + ) + styles = getSampleStyleSheet() + story = [Paragraph(title, styles["Title"]), Spacer(1, 0.2 * inch)] + if charts: + story.append(Paragraph("Project Execution Summary", styles["Heading2"])) + story.append(Spacer(1, 0.1 * inch)) + + # Summary Table instead of charts + table_data = [["Metric / Category", "Value"]] + + # Tasks Status + status_counts = {row["label"]: row["value"] for row in charts.get("status", [])} + for label, val in status_counts.items(): + table_data.append([f"Tasks: {label}", str(val)]) + + # Categories + for cat in charts.get("categories", []): + table_data.append([f"Type: {cat['label']}", str(cat['value'])]) + + table = Table(table_data, colWidths=[3.5*inch, 1.5*inch]) + table.setStyle(TableStyle([ + ('BACKGROUND', (0,0), (-1,0), colors.HexColor("#6e59ff")), + ('TEXTCOLOR', (0,0), (-1,0), colors.whitesmoke), + ('ALIGN', (0,0), (-1,-1), 'LEFT'), + ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'), + ('BOTTOMPADDING', (0,0), (-1,0), 10), + ('BACKGROUND', (0,1), (-1,-1), colors.HexColor("#f8fafc")), + ('GRID', (0,0), (-1,-1), 0.5, colors.grey), + ('FONTSIZE', (0,0), (-1,-1), 9), + ])) + story.append(table) + story.append(Spacer(1, 0.3 * inch)) + + for raw_line in content.splitlines(): + line = raw_line.strip() + if not line: + story.append(Spacer(1, 0.1 * inch)) + continue + if line.startswith("# "): + story.append(Paragraph(line[2:], styles["Title"])) + elif line.startswith("## "): + story.append(Paragraph(line[3:], styles["Heading2"])) + elif line.startswith("### "): + story.append(Paragraph(line[4:], styles["Heading3"])) + elif line.startswith("- "): + story.append(Paragraph(f"• {line[2:]}", styles["BodyText"])) + else: + story.append(Paragraph(line, styles["BodyText"])) + + doc.build(story) + return buffer.getvalue() + +class DebateRequest(BaseModel): + + task_id: str + agent_a_id: str + agent_b_id: str + +@router.post("/debate") +async def start_debate(request: DebateRequest, background_tasks: BackgroundTasks): + """ + Starts a debate between two agents for a specific task. + """ + background_tasks.add_task( + orchestrator_service.run_debate, + request.task_id, + request.agent_a_id, + request.agent_b_id + ) + return {"message": "Debate started in background"} + + +@router.post("/projects/{project_id}/run") +async def run_project_orchestrator(project_id: str, background_tasks: BackgroundTasks): + """ + Runs all queued tasks for a project in priority order. + """ + background_tasks.add_task(orchestrator_service.run_project, project_id) + return {"message": "Project orchestrator started", "project_id": project_id} + +@router.get("/projects/{project_id}/final-report") +async def get_project_final_report(project_id: str, variant: str = "full"): + """ + Builds a consolidated report from all approved task outputs. + """ + try: + return await orchestrator_service.build_final_report(project_id, variant) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + +@router.get("/projects/{project_id}/final-report.pdf") +async def download_project_final_report_pdf(project_id: str, variant: str = "full"): + """ + Downloads the selected report variant as a PDF. + """ + try: + result = await orchestrator_service.build_final_report(project_id, variant) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + title = f"{result['project_name']} - {result['variant']} report" + pdf = _report_pdf_bytes(title, result["report"], result.get("charts")) + filename = f"{_safe_filename(result['project_name'])}_{_safe_filename(result['variant'])}.pdf" + return Response( + content=pdf, + media_type="application/pdf", + headers={"Content-Disposition": f'attachment; filename="{filename}"'} + ) diff --git a/backend/scratch/check_db.py b/backend/scratch/check_db.py new file mode 100644 index 0000000000000000000000000000000000000000..463b7256718e13456d1084907a501c47be2277e9 --- /dev/null +++ b/backend/scratch/check_db.py @@ -0,0 +1,22 @@ +import os +from supabase import create_client +from dotenv import load_dotenv + +load_dotenv() + +supabase_url = os.getenv("SUPABASE_URL") +supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY") +supabase = create_client(supabase_url, supabase_key) + +def check_logs(): + try: + res = supabase.table("agent_logs").select("*").order("created_at", desc=True).limit(20).execute() + print(f"Total logs retrieved: {len(res.data)}") + for log in res.data: + print(f"[{log['created_at']}] {log['action']}: {log['content'][:50]}...") + + except Exception as e: + print(f"Error accessing agent_logs: {e}") + +if __name__ == "__main__": + check_logs() diff --git a/backend/scratch/create_comparison_project.py b/backend/scratch/create_comparison_project.py new file mode 100644 index 0000000000000000000000000000000000000000..58df3b5a7cc23eff7241c6e479e2c0f74c28f13f --- /dev/null +++ b/backend/scratch/create_comparison_project.py @@ -0,0 +1,61 @@ +import os +from supabase import create_client +from dotenv import load_dotenv + +load_dotenv() + +supabase_url = os.getenv("SUPABASE_URL") +supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY") +supabase = create_client(supabase_url, supabase_key) + +def create_project(): + try: + # 1. Try to get owner_id from existing projects first + existing_projects = supabase.table("projects").select("owner_id").limit(1).execute() + user_id = None + if existing_projects.data and existing_projects.data[0].get("owner_id"): + user_id = existing_projects.data[0]["owner_id"] + + if not user_id: + # Fallback to profiles + users = supabase.table("profiles").select("id").limit(1).execute() + if users.data: + user_id = users.data[0]["id"] + + if not user_id: + print("No valid owner_id found in projects or profiles. The project will be created without owner and might not be visible.") + else: + print(f"Using owner_id: {user_id}") + + # 2. Create Project + project_data = { + "name": "Aubm Competitor Analysis", + "description": "Deep dive into the multi-agent orchestration market to identify Aubm's unique value proposition and feature gaps.", + "status": "active", + "context": "Focus on developer experience, visual observability, and the 'Agent Debate' mechanism as key differentiators." + } + if user_id: + project_data["owner_id"] = user_id + + project_res = supabase.table("projects").insert(project_data).execute() + project_id = project_res.data[0]["id"] + print(f"Created Project: {project_id}") + + # 3. Create Tasks + tasks = [ + {"title": "Identify Top 5 Competitors", "description": "Research and list 5 similar multi-agent orchestration platforms (e.g., CrewAI, AutoGen, LangGraph, PydanticAI).", "status": "todo", "project_id": project_id}, + {"title": "Feature Comparison Matrix", "description": "Create a detailed matrix comparing Aubm's core features (Project Decomposition, Agent Debate, Real-time Console) against identified competitors.", "status": "todo", "project_id": project_id}, + {"title": "Pricing Model Analysis", "description": "Analyze how competitors charge (SaaS, Open Source, API usage) and recommend a competitive strategy for Aubm.", "status": "todo", "project_id": project_id}, + {"title": "UI/UX Aesthetic Audit", "description": "Evaluate the visual aesthetics and ease of use of competitors compared to Aubm's premium dashboard. Look for glassmorphism and animations.", "status": "todo", "project_id": project_id}, + {"title": "Technical Architecture Deep-Dive", "description": "Investigate the underlying tech stacks (Python vs TS, Vector DBs used, Orchestration logic) of top competitors.", "status": "todo", "project_id": project_id}, + {"title": "SWOT Analysis & Strategy Report", "description": "Compile all findings into a comprehensive report with a SWOT analysis and strategic recommendations for the next 6 months.", "status": "todo", "project_id": project_id} + ] + + supabase.table("tasks").insert(tasks).execute() + print(f"Added {len(tasks)} tasks to the project.") + + except Exception as e: + print(f"Error: {e}") + +if __name__ == "__main__": + create_project() diff --git a/backend/scratch/find_user.py b/backend/scratch/find_user.py new file mode 100644 index 0000000000000000000000000000000000000000..82765bfc3806a401ae9e457093c89103bc55a5a9 --- /dev/null +++ b/backend/scratch/find_user.py @@ -0,0 +1,24 @@ +import os +from supabase import create_client +from dotenv import load_dotenv + +load_dotenv() + +supabase_url = os.getenv("SUPABASE_URL") +supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY") +supabase = create_client(supabase_url, supabase_key) + +def check_users(): + # Try different tables where users might be + tables = ["profiles", "users", "team_members"] + for table in tables: + try: + res = supabase.table(table).select("id").limit(1).execute() + print(f"Table {table} count: {len(res.data)}") + if res.data: + print(f"Sample ID: {res.data[0]['id']}") + except Exception as e: + print(f"Error checking {table}: {e}") + +if __name__ == "__main__": + check_users() diff --git a/backend/scratch/fix_logs_rls.py b/backend/scratch/fix_logs_rls.py new file mode 100644 index 0000000000000000000000000000000000000000..b6866a5455cba2f28634beed2a7f92527264bf0f --- /dev/null +++ b/backend/scratch/fix_logs_rls.py @@ -0,0 +1,33 @@ +import os +from supabase import create_client +from dotenv import load_dotenv + +load_dotenv() + +supabase_url = os.getenv("SUPABASE_URL") +supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY") +supabase = create_client(supabase_url, supabase_key) + +sql = """ +ALTER TABLE agent_logs ENABLE ROW LEVEL SECURITY; +DROP POLICY IF EXISTS "Enable read access for all users" ON agent_logs; +CREATE POLICY "Enable read access for all users" ON agent_logs FOR SELECT USING (true); +""" + +# Note: This assumes an 'exec_sql' RPC exists, which is common in many setups. +# If not, I'll have to find another way. +try: + # Actually, let's try a different approach if RPC fails. + # We can try to use the REST API to check if it works. + print("Attempting to set RLS policy...") + # Since I don't have direct SQL access via the client without RPC, + # I'll assume the user might need to do this in the dashboard or I'll try to find an RPC. + + # Let's check if the client can read with anon key. + anon_key = os.getenv("SUPABASE_ANON_KEY") + anon_s = create_client(supabase_url, anon_key) + res = anon_s.table("agent_logs").select("*").limit(1).execute() + print(f"Anon read test: {'Success' if not res.data else 'Empty/Restricted'}") + +except Exception as e: + print(f"Error: {e}") diff --git a/backend/services/agent_runner_service.py b/backend/services/agent_runner_service.py new file mode 100644 index 0000000000000000000000000000000000000000..f2c997bff261d1b99174b4be8227a5a9cc85a09a --- /dev/null +++ b/backend/services/agent_runner_service.py @@ -0,0 +1,210 @@ +import logging +from datetime import datetime, timezone +from services.supabase_service import supabase +from services.audit_service import audit_service +from agents.agent_factory import AgentFactory +from services.semantic_backprop import semantic_backprop +from services.output_quality import build_quality_instructions, validate_output + +logger = logging.getLogger("agent_runner_service") + +class AgentRunnerService: + @staticmethod + async def run_agent_task( + task: dict, + agent_data: dict, + *, + include_semantic_context: bool = False, + start_action: str = "execution_start", + start_content: str | None = None, + complete_action: str = "execution_complete", + complete_content: str = "Agent successfully completed the task and produced output.", + update_task: bool = True + ) -> tuple[dict, str]: + task_id = task["id"] + project_id = task["project_id"] + run_id = None + + if update_task: + supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute() + + try: + run_res = supabase.table("task_runs").insert({ + "task_id": task_id, + "agent_id": agent_data["id"], + "status": "running" + }).execute() + run_id = run_res.data[0]["id"] + + agent = AgentFactory.get_agent( + provider=agent_data["api_provider"], + name=agent_data["name"], + role=agent_data["role"], + model=agent_data["model"], + system_prompt=agent_data.get("system_prompt") + ) + + context_res = supabase.table("tasks").select("title, output_data") \ + .eq("project_id", project_id) \ + .eq("status", "done") \ + .execute() + context = context_res.data if context_res.data else [] + + project_data = task.get("project") + if not isinstance(project_data, dict): + project_res = ( + supabase.table("projects") + .select("name,description,context") + .eq("id", project_id) + .single() + .execute() + ) + project_data = project_res.data if project_res and project_res.data else {} + quality_task = {**task, "project": project_data} + + extra_context = "" + if include_semantic_context: + extra_context = await semantic_backprop.get_project_context(project_id, task_id) + + import time + import hashlib + + # Simple in-memory cache for the session (could be persistent later) + if not hasattr(AgentRunnerService, "_task_cache"): + AgentRunnerService._task_cache = {} + + # 1. Create a cache key based on task, agent (model + system prompt), and context + cache_input = f"{task['id']}-{agent_data['model']}-{agent_data.get('system_prompt', '')}-{task.get('description')}-{str(context)}-{extra_context}" + cache_key = hashlib.md5(cache_input.encode()).hexdigest() + + # 2. Check Cache + if cache_key in AgentRunnerService._task_cache: + logger.info(f"Cache hit for task {task_id}. Skipping LLM call.") + cached_result = AgentRunnerService._task_cache[cache_key] + + # Still log the "start" for UI consistency + agent_name = agent_data.get('name', 'Agent') + log_msg = start_content or f"Agent {agent_name} resuming task" + supabase.table("agent_logs").insert({ + "task_id": task_id, + "run_id": run_id, + "action": start_action, + "content": f"[CACHE HIT] {log_msg}" + }).execute() + + if update_task: + supabase.table("tasks").update({ + "status": "awaiting_approval", + "output_data": cached_result + }).eq("id", task_id).execute() + + return cached_result, run_id + + # 3. Log Start + supabase.table("agent_logs").insert({ + "task_id": task_id, + "run_id": run_id, + "action": start_action, + "content": start_content or f"Agent {agent_data['name']} starting task: {task['title']}" + }).execute() + + # 4. Execute Run with timing + start_time = time.time() + task_instructions = task.get("description") or task["title"] + task_instructions = f"{task_instructions}\n\n{build_quality_instructions(quality_task)}" + result = await agent.run(task_instructions, context, extra_context=extra_context) + duration = time.time() - start_time + + if result.get("status") == "error": + raise RuntimeError(result.get("error") or "Agent returned an error result.") + + # 5. Security Sanitization (Defense in Depth) + raw_out = str(result.get("raw_output", "")) + suspicious_patterns = ["rm -rf", "mkfs", "dd if=", "curl", "wget", "chmod 777", "> /dev/sda"] + for pattern in suspicious_patterns: + if pattern in raw_out: + logger.warning(f"SECURITY: Suspicious pattern '{pattern}' detected in agent output for task {task_id}.") + result["security_warning"] = f"Output sanitized: suspicious pattern '{pattern}' detected." + # We don't block yet, but we flag it. + + quality_review = validate_output(quality_task, result) + result["quality_review"] = quality_review + + # 6. Save to Cache + AgentRunnerService._task_cache[cache_key] = result + + if update_task: + supabase.table("tasks").update({ + "status": "awaiting_approval", + "output_data": result + }).eq("id", task_id).execute() + + # 7. Update Run Status + supabase.table("task_runs").update({ + "status": "completed", + "finished_at": datetime.now(timezone.utc).isoformat(), + "duration_seconds": round(duration, 2) + }).eq("id", run_id).execute() + + # 8. Log Completion with Metrics + supabase.table("agent_logs").insert({ + "task_id": task_id, + "run_id": run_id, + "action": complete_action, + "content": f"{complete_content} (Execution time: {duration:.2f}s)" + }).execute() + + if not quality_review["approved"]: + supabase.table("agent_logs").insert({ + "task_id": task_id, + "run_id": run_id, + "action": "quality_review_failed", + "content": f"Quality review failed: {', '.join(quality_review['fail_reasons'])}" + }).execute() + + return result, run_id + + except Exception as e: + logger.error(f"Error executing task {task_id}: {str(e)}") + if run_id: + supabase.table("task_runs").update({ + "status": "failed", + "finished_at": datetime.now(timezone.utc).isoformat() + }).eq("id", run_id).execute() + + if update_task: + supabase.table("tasks").update({ + "status": "failed", + "output_data": {"error": str(e)} + }).eq("id", task_id).execute() + + # LOG ERROR TO AGENT CONSOLE + supabase.table("agent_logs").insert({ + "task_id": task_id, + "run_id": run_id, + "action": "execution_failed", + "content": f"ERROR: {str(e)}" + }).execute() + + raise e + + @staticmethod + async def execute_agent_logic(task: dict, agent_data: dict): + task_id = task["id"] + try: + await AgentRunnerService.run_agent_task( + task, + agent_data, + include_semantic_context=True + ) + + await audit_service.log_action( + user_id=None, + action="agent_task_completed", + agent_id=agent_data["id"], + task_id=task_id, + metadata={"model": agent_data["model"]} + ) + + except Exception: + raise diff --git a/backend/services/audit_service.py b/backend/services/audit_service.py new file mode 100644 index 0000000000000000000000000000000000000000..df517c90c55e37906401b185c40310d6ba72fdb9 --- /dev/null +++ b/backend/services/audit_service.py @@ -0,0 +1,31 @@ +from services.supabase_service import supabase +from typing import Dict, Any, Optional +import logging + +logger = logging.getLogger("uvicorn") + +class AuditService: + @staticmethod + async def log_action( + user_id: Optional[str], + action: str, + agent_id: Optional[str] = None, + task_id: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None + ): + """ + Records an action in the audit_logs table. + """ + try: + data = { + "user_id": user_id, + "action": action, + "agent_id": agent_id, + "task_id": task_id, + "metadata": metadata or {} + } + supabase.table("audit_logs").insert(data).execute() + except Exception as e: + logger.error(f"AuditService error: {str(e)}") + +audit_service = AuditService() diff --git a/backend/services/config.py b/backend/services/config.py new file mode 100644 index 0000000000000000000000000000000000000000..6684c8177e42f084503b066f99a63d1fabef651b --- /dev/null +++ b/backend/services/config.py @@ -0,0 +1,101 @@ +import os +from pydantic_settings import BaseSettings +from typing import Optional, Dict, Any +from supabase import create_client, Client + +class Settings(BaseSettings): + # Supabase + SUPABASE_URL: str = "" + SUPABASE_SERVICE_ROLE_KEY: str = "" + + # AI Providers + OPENAI_API_KEY: Optional[str] = None + GROQ_API_KEY: Optional[str] = None + GEMINI_API_KEY: Optional[str] = None + ANTHROPIC_API_KEY: Optional[str] = None + AMD_API_KEY: Optional[str] = None + TAVILY_API_KEY: Optional[str] = None + + # App Config + TASK_QUEUE_EMBEDDED_WORKER: bool = True + OUTPUT_LANGUAGE: str = "en" + PORT: int = 8000 + SENTRY_DSN: Optional[str] = None + + model_config = { + "env_file": ".env", + "extra": "ignore" + } + +settings = Settings() + +class ConfigService: + """ + Manages application-wide settings stored in Supabase with local fallback defaults. + Borrowed from AgentCollab for enhanced flexibility. + """ + _cache: Dict[str, Any] = {} + _supabase: Client = None + + @classmethod + def _get_supabase(cls): + if not cls._supabase: + if not settings.SUPABASE_URL or not settings.SUPABASE_SERVICE_ROLE_KEY: + return None + cls._supabase = create_client(settings.SUPABASE_URL, settings.SUPABASE_SERVICE_ROLE_KEY) + return cls._supabase + + # Defaults used when DB has no config entry for a provider + _DEFAULTS: Dict[str, Any] = { + "groq": {"enabled": True, "default_model": "llama-3.3-70b-versatile", "temperature": 0.7, "max_tokens": 4096}, + "openai": {"enabled": True, "default_model": "gpt-4o", "temperature": 0.7, "max_tokens": 4096}, + "openrouter": {"enabled": True, "default_model": "google/gemini-2.0-flash", "temperature": 0.7, "max_tokens": 8192}, + "gemini": {"enabled": True, "default_model": "gemini-2.0-flash", "temperature": 0.7, "max_tokens": 8192}, + "amd": {"enabled": True, "default_model": "gpt-4o", "temperature": 0.7, "max_tokens": 4096, "base_url": "https://inference.do-ai.run/v1"}, + "ollama": {"enabled": True, "default_model": "llama3.1:8b", "temperature": 0.7, "base_url": "http://localhost:11434"}, + } + + @classmethod + def get_provider_config(cls, provider: str) -> Dict[str, Any]: + """Returns config for a provider from cache, DB, then defaults.""" + cache_key = f"provider:{provider}" + if cache_key in cls._cache: + return cls._cache[cache_key] + + db = cls._get_supabase() + if db: + try: + resp = db.table("app_config").select("*").eq("key", provider).execute() + if resp.data and len(resp.data) > 0: + cls._cache[cache_key] = resp.data[0]["value"] + return cls._cache[cache_key] + except Exception: + pass # Fall through to defaults + + result = cls._DEFAULTS.get(provider, {}) + cls._cache[cache_key] = result + return result + + @classmethod + def get_global_setting(cls, key: str, default: Any = None) -> Any: + cache_key = f"global:{key}" + if cache_key in cls._cache: + return cls._cache[cache_key] + + db = cls._get_supabase() + if db: + try: + resp = db.table("app_config").select("*").eq("key", key).execute() + if resp.data and len(resp.data) > 0: + cls._cache[cache_key] = resp.data[0]["value"] + return cls._cache[cache_key] + except Exception: + pass + + return default + + @classmethod + def invalidate_cache(cls) -> None: + cls._cache.clear() + +config_service = ConfigService() diff --git a/backend/services/orchestrator_service.py b/backend/services/orchestrator_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e27d8e569680a110941aacf82fbe234d13b95178 --- /dev/null +++ b/backend/services/orchestrator_service.py @@ -0,0 +1,773 @@ +from services.supabase_service import supabase +from agents.agent_factory import AgentFactory +import json +import logging +import re +from services.config import settings +from services.agent_runner_service import AgentRunnerService +from services.output_quality import clean_report_text, dedupe_lines, filter_report_sections, validate_output + +logger = logging.getLogger("uvicorn") + +NOISY_REPORT_KEYS = { + "raw_text", + "sampleBackendCode", + "sampleUploadSnippet", + "sampleSearchEndpoint", + "sampleRedisCartHelper", + "sampleWebhookHandler", + "sampleStateMachine", + "repositoryStructure", + "wireframes", + "dataModel", + "userStories", +} + +def _humanize_key(key: str) -> str: + return key.replace("_", " ").replace("-", " ").strip().title() + +def _format_value_for_report(value, level: int = 0) -> list[str]: + if value is None: + return ["Not specified."] + + if isinstance(value, (str, int, float, bool)): + return [str(value)] + + if isinstance(value, list): + lines: list[str] = [] + for item in value: + if isinstance(item, dict): + item_lines = _format_value_for_report(item, level + 1) + if item_lines: + lines.append(f"- {item_lines[0]}") + lines.extend(f" {line}" for line in item_lines[1:]) + elif isinstance(item, list): + nested = _format_value_for_report(item, level + 1) + lines.extend(f"- {line}" for line in nested) + else: + lines.append(f"- {item}") + return lines or ["No items."] + + if isinstance(value, dict): + lines: list[str] = [] + for key, item in value.items(): + if str(key) in NOISY_REPORT_KEYS: + continue + title = _humanize_key(str(key)) + if isinstance(item, dict): + lines.append(f"{title}:") + lines.extend(f" {line}" for line in _format_value_for_report(item, level + 1)) + elif isinstance(item, list): + lines.append(f"{title}:") + lines.extend(f" {line}" for line in _format_value_for_report(item, level + 1)) + else: + lines.append(f"{title}: {item}") + return lines or ["No details."] + + return [str(value)] + + +def _extract_json_payload(text: str): + stripped = text.strip() + if stripped.startswith("```"): + stripped = stripped.strip("`") + if stripped.lower().startswith("json"): + stripped = stripped[4:].strip() + try: + return json.loads(stripped) + except Exception: + match = re.search(r"```json\s*(.*?)\s*```", text, re.IGNORECASE | re.DOTALL) + if match: + try: + return json.loads(match.group(1)) + except Exception: + return None + return None + +def _format_output_for_report(output_data) -> str: + if not output_data: + return "No approved output was saved for this task." + + if isinstance(output_data, dict): + primary = ( + output_data.get("data") + or output_data.get("final") + or output_data.get("raw_output") + or output_data + ) + else: + primary = output_data + + if isinstance(primary, str): + parsed = _extract_json_payload(primary) + if parsed is not None: + return clean_report_text(dedupe_lines("\n".join(_format_value_for_report(parsed)))) + return clean_report_text(dedupe_lines(primary)) + + return clean_report_text(dedupe_lines("\n".join(_format_value_for_report(primary)))) + + +def _is_empty_curated_text(text: str) -> bool: + normalized = (text or "").strip().lower() + return normalized in { + "", + "no approved output was saved for this task.", + "{}", + "[]", + } + + +def _format_conclusion_payload(data: dict) -> str: + conclusion = data.get("strategicConclusion") or data.get("conclusion") or data.get("content") or "" + next_steps = data.get("nextSteps") or data.get("next_steps") or [] + + lines: list[str] = [] + if isinstance(conclusion, str) and conclusion.strip(): + lines.append(conclusion.strip()) + + if isinstance(next_steps, list) and next_steps: + lines.append("") + lines.append("Next steps:") + for step in next_steps[:5]: + if isinstance(step, str) and step.strip(): + lines.append(f"- {step.strip()}") + + return "\n".join(lines).strip() or "\n".join(_format_value_for_report(data)) + + +def _has_usable_output(output_data) -> bool: + if not output_data: + return False + if isinstance(output_data, dict): + if output_data.get("error"): + return False + primary = output_data.get("data") + if primary in (None, "", [], {}): + return False + return True + +def _output_text(output_data) -> str: + return _format_output_for_report(output_data).lower() + +def _build_report_charts(tasks: list[dict]) -> dict: + total = len(tasks) + done = sum(1 for task in tasks if task.get("status") == "done") + failed = sum(1 for task in tasks if task.get("status") == "failed") + pending = max(total - done - failed, 0) + + priority_counts: dict[str, int] = {} + for task in tasks: + priority = str(task.get("priority") if task.get("priority") is not None else 0) + priority_counts[priority] = priority_counts.get(priority, 0) + 1 + + categories = { + "Market": ("market", "competitor", "customer", "segment", "demand"), + "Product": ("product", "mvp", "feature", "design", "scope"), + "Revenue": ("revenue", "price", "pricing", "margin", "commission"), + "Operations": ("operation", "process", "logistic", "support", "fulfillment"), + "Risk": ("risk", "threat", "failure", "weak", "mitigation") + } + category_counts = {name: 0 for name in categories} + risk_mentions = 0 + + for task in tasks: + text = f"{task.get('title', '')} {task.get('description', '')} {_output_text(task.get('output_data'))}" + risk_mentions += sum(text.count(term) for term in categories["Risk"]) + for category, terms in categories.items(): + if any(term in text for term in terms): + category_counts[category] += 1 + + opportunity_score = 85 if total and done == total else round((done / total) * 85) if total else 0 + risk_score = min(95, 35 + risk_mentions * 3) + readiness_score = round((done / total) * 100) if total else 0 + + return { + "status": [ + {"label": "Approved", "value": done}, + {"label": "Pending", "value": pending}, + {"label": "Failed", "value": failed} + ], + "priorities": [ + {"label": f"Priority {key}", "value": value} + for key, value in sorted(priority_counts.items(), key=lambda item: int(item[0]) if item[0].isdigit() else 0, reverse=True) + ], + "categories": [ + {"label": label, "value": value} + for label, value in category_counts.items() + ], + "scores": [ + {"label": "Readiness", "value": readiness_score}, + {"label": "Opportunity", "value": opportunity_score}, + {"label": "Risk", "value": risk_score} + ] + } + +REPORT_VARIANTS = { + "full": { + "title": "Final Report", + "agent_terms": [], + "fallback_heading": "Approved Work Summary", + "prompt": "" + }, + "brief": { + "title": "Short Brief", + "agent_terms": ["brief", "summary", "writer"], + "fallback_heading": "Short Brief", + "prompt": ( + "Create a concise executive brief from the approved project work. " + "Use plain English, no JSON, no code blocks. Include: objective, main findings, recommended next steps, and key risks. " + "Keep it short and decision-oriented. Do not invent entities, metrics, or placeholders." + ) + }, + "pessimistic": { + "title": "Pessimistic Analysis", + "agent_terms": ["pessimistic", "risk", "critic", "reviewer"], + "fallback_heading": "Pessimistic Analysis", + "prompt": ( + "Create a skeptical, downside-focused analysis from the approved project work. " + "Use plain English, no JSON, no code blocks. Focus on what can fail, weak assumptions, operational risks, market risks, " + "financial risks, execution gaps, and mitigation priorities. Do not invent entities, metrics, or placeholders." + ) + } +} + +class OrchestratorService: + """ + Handles complex multi-agent workflows like Debates and Peer Reviews. + """ + + async def run_debate(self, task_id: str, agent_a_id: str, agent_b_id: str): + """ + Executes a debate between two agents for a specific task. + """ + try: + # 1. Fetch task and agents + task = supabase.table("tasks").select("*").eq("id", task_id).single().execute().data + agent_a_data = supabase.table("agents").select("*").eq("id", agent_a_id).single().execute().data + agent_b_data = supabase.table("agents").select("*").eq("id", agent_b_id).single().execute().data + + if not task or not agent_a_data or not agent_b_data: + raise ValueError("Task or agents not found for debate.") + + # Update status to in_progress + supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute() + + # 2. Agent A generates initial response + initial_res, _ = await AgentRunnerService.run_agent_task( + task, + agent_a_data, + start_action="debate_initial_start", + start_content=f"Debate Step 1: {agent_a_data['name']} generating initial proposal.", + complete_action="debate_initial_complete", + update_task=False + ) + + # 3. Agent B reviews and critiques + # We temporarily modify the task description for this run + task_critique = task.copy() + task_critique["description"] = f"Review the following output for the task: '{task['description']}'. Provide constructive critique and identify errors.\n\nOutput: {json.dumps(initial_res['data'])}" + + critique_res, _ = await AgentRunnerService.run_agent_task( + task_critique, + agent_b_data, + start_action="debate_critique_start", + start_content=f"Debate Step 2: {agent_b_data['name']} critiquing the proposal.", + complete_action="debate_critique_complete", + update_task=False + ) + + # 4. Agent A refines based on critique + task_refinement = task.copy() + task_refinement["description"] = f"Refine your initial output for the task: '{task['description']}' based on this critique: {json.dumps(critique_res['data'])}" + + final_res, _ = await AgentRunnerService.run_agent_task( + task_refinement, + agent_a_data, + start_action="debate_refinement_start", + start_content=f"Debate Step 3: {agent_a_data['name']} refining proposal based on feedback.", + complete_action="debate_refinement_complete", + update_task=False + ) + + # 5. Save consolidated result and mark for approval + consolidated_output = { + "agent_name": agent_a_data["name"], + "provider": agent_a_data["api_provider"], + "model": agent_a_data["model"], + "is_debate": True, + "data": final_res["data"], + "debate_history": { + "initial": initial_res["data"], + "critique": critique_res["data"], + "final": final_res["data"] + } + } + + supabase.table("tasks").update({ + "status": "awaiting_approval", + "output_data": consolidated_output + }).eq("id", task_id).execute() + + logger.info(f"Debate completed for task {task_id}") + + except Exception as e: + logger.error(f"Debate failed: {str(e)}") + supabase.table("tasks").update({ + "status": "failed", + "output_data": {"error": str(e)} + }).eq("id", task_id).execute() + + # LOG ERROR TO AGENT CONSOLE + supabase.table("agent_logs").insert({ + "task_id": task_id, + "action": "debate_failed", + "content": f"DEBATE ERROR: {str(e)}" + }).execute() + + async def run_project(self, project_id: str): + """ + Runs queued tasks in a project sequentially. Unassigned tasks are assigned + to the first available project-owner or global agent. + """ + project = supabase.table("projects").select("*").eq("id", project_id).single().execute().data + if not project: + raise ValueError(f"Project not found: {project_id}") + + owner_id = project.get("owner_id") + tasks = ( + supabase.table("tasks") + .select("*") + .eq("project_id", project_id) + .eq("status", "todo") + .order("priority", desc=True) + .order("created_at", desc=False) + .execute() + .data + or [] + ) + + # Check if ANY tasks exist for this project (regardless of status) to avoid re-decomposing + all_tasks_res = supabase.table("tasks").select("id", count="exact").eq("project_id", project_id).limit(1).execute() + has_any_tasks = all_tasks_res.count > 0 if all_tasks_res.count is not None else len(all_tasks_res.data) > 0 + + # Automatic Decomposition: Only if no tasks exist AT ALL + if not has_any_tasks: + logger.info(f"No tasks found for project {project_id}. Triggering auto-decomposition.") + await self.decompose_project(project_id) + # Re-fetch tasks after decomposition + tasks = ( + supabase.table("tasks") + .select("*") + .eq("project_id", project_id) + .in_("status", ["todo", "failed"]) + .order("priority", desc=True) + .order("created_at", desc=False) + .execute() + .data + or [] + ) + + agents = supabase.table("agents").select("*").execute().data or [] + available_agents = [ + agent for agent in agents + if agent.get("user_id") in (None, owner_id) + ] + + completed = 0 + failed = 0 + + for task in tasks: + try: + agent_data = self._resolve_agent(task, available_agents) + if not agent_data: + raise ValueError("No available agent for task") + + if not task.get("assigned_agent_id"): + supabase.table("tasks").update({ + "assigned_agent_id": agent_data["id"] + }).eq("id", task["id"]).execute() + task["assigned_agent_id"] = agent_data["id"] + + await self._run_task(task, agent_data) + completed += 1 + except Exception as exc: + failed += 1 + logger.error(f"Project orchestration task failed: {str(exc)}") + supabase.table("tasks").update({ + "status": "failed", + "output_data": {"error": str(exc)} + }).eq("id", task["id"]).execute() + + return { + "project_id": project_id, + "queued_tasks": len(tasks), + "completed": completed, + "failed": failed, + } + + def _select_report_agent(self, project: dict, variant: str): + config = REPORT_VARIANTS.get(variant, REPORT_VARIANTS["full"]) + terms = config["agent_terms"] + if not terms: + return None + + owner_id = project.get("owner_id") + agents = supabase.table("agents").select("*").execute().data or [] + available_agents = [ + agent for agent in agents + if agent.get("user_id") in (None, owner_id) + ] + + return next( + ( + agent for agent in available_agents + if any(term in f"{agent.get('name', '')} {agent.get('role', '')}".lower() for term in terms) + ), + available_agents[0] if available_agents else None + ) + + async def _generate_report_variant_with_agent(self, project: dict, report: str, variant: str): + agent_data = self._select_report_agent(project, variant) + if not agent_data: + return None + + config = REPORT_VARIANTS[variant] + agent = AgentFactory.get_agent( + provider=agent_data["api_provider"], + name=agent_data["name"], + role=agent_data["role"], + model=agent_data["model"], + system_prompt=agent_data.get("system_prompt") + ) + result = await agent.run(f"{config['prompt']}\n\nApproved project material:\n{report}", []) + if result.get("status") == "error": + raise RuntimeError(result.get("error") or "Report agent returned an error.") + + data = result.get("data") + if isinstance(data, dict): + for key in ("brief", "analysis", "report", "summary", "content"): + if isinstance(data.get(key), str): + return data[key] + return "\n".join(_format_value_for_report(data)) + if isinstance(data, str): + return data + return result.get("raw_output") + + def _build_fallback_variant(self, project: dict, tasks: list[dict], variant: str): + config = REPORT_VARIANTS[variant] + lines = [ + f"# {config['title']}: {project['name']}", + "", + "## Project Brief", + project.get("description") or "No project description provided.", + "", + f"## {config['fallback_heading']}" + ] + + if variant == "brief": + lines.extend([ + f"All {len(tasks)} approved tasks have been consolidated.", + "The project is ready for decision review based on the approved task outputs.", + "", + "Recommended next steps:", + "- Validate the highest-impact assumptions with real users or customers.", + "- Prioritize the smallest launch scope that proves demand.", + "- Convert approved outputs into an execution backlog with owners and dates." + ]) + return "\n".join(lines) + + if variant == "pessimistic": + lines.extend([ + "This project can still fail even with all tasks approved.", + "", + "Primary downside risks:", + "- Approved task outputs may be internally consistent but unvalidated by the market.", + "- Revenue, conversion, operational, and adoption assumptions may be too optimistic.", + "- Execution scope can expand faster than the team can deliver.", + "- Competitors can respond with pricing, distribution, or trust advantages.", + "", + "Mitigation priorities:", + "- Validate demand before building broad feature scope.", + "- Stress-test unit economics and support costs.", + "- Define kill criteria before committing more resources." + ]) + return "\n".join(lines) + + return None + + def _quality_approved_tasks(self, tasks: list[dict], project: dict) -> tuple[list[dict], list[dict]]: + approved: list[dict] = [] + excluded: list[dict] = [] + for task in tasks: + output_data = task.get("output_data") or {} + if not _has_usable_output(output_data): + excluded.append({ + "title": task.get("title", "Untitled task"), + "reasons": ["Task has no usable approved output."] + }) + continue + task_with_project = {**task, "project": project} + quality_review = output_data.get("quality_review") if isinstance(output_data, dict) else None + if not quality_review and isinstance(output_data, dict): + quality_review = validate_output(task_with_project, output_data) + if quality_review and not quality_review.get("approved", False): + excluded.append({ + "title": task.get("title", "Untitled task"), + "reasons": quality_review.get("fail_reasons") or ["Failed quality review."] + }) + continue + approved.append(task) + return approved, excluded + + def _curate_task_output(self, output_data) -> tuple[str, list[str]]: + text = _format_output_for_report(output_data) + text = clean_report_text(dedupe_lines(text)) + text, excluded_lines = filter_report_sections(text) + return text or "No approved output was saved for this task.", excluded_lines + + async def build_final_report(self, project_id: str, variant: str = "full"): + variant = variant if variant in REPORT_VARIANTS else "full" + project = supabase.table("projects").select("*").eq("id", project_id).single().execute().data + if not project: + raise ValueError(f"Project not found: {project_id}") + + tasks = ( + supabase.table("tasks") + .select("title,description,status,priority,output_data,created_at") + .eq("project_id", project_id) + .order("priority", desc=True) + .order("created_at", desc=False) + .execute() + .data + or [] + ) + + if not tasks: + raise ValueError("Project has no tasks to summarize.") + + incomplete = [task for task in tasks if task.get("status") != "done"] + if incomplete: + raise ValueError(f"Final report is available after all tasks are approved. Pending tasks: {len(incomplete)}") + + curated_tasks, excluded_tasks = self._quality_approved_tasks(tasks, project) + if not curated_tasks: + raise ValueError("No approved task outputs passed quality validation for final reporting.") + + # 0. Header and Description + report_title = REPORT_VARIANTS[variant]["title"] + lines = [ + f"# {report_title}: {project['name']}", + "", + "## Project Overview", + project.get("description") or "No description provided.", + "" + ] + + # Add Context if exists + if project.get("context"): + lines.extend(["## Context", project["context"], ""]) + + lines.extend(["## Execution Summary", ""]) + + # We will add the tabular summary later in the UI or via charts, + # but for the text report, we include the approved work summary. + lines.extend(["## Approved Work Summary", ""]) + + report_exclusions: list[str] = [] + kept_task_count = 0 + for task in curated_tasks: + curated_text, excluded_lines = self._curate_task_output(task.get("output_data")) + report_exclusions.extend(excluded_lines) + if _is_empty_curated_text(curated_text): + excluded_tasks.append({ + "title": task.get("title", "Untitled task"), + "reasons": ["Task output became empty after quality filtering."] + }) + continue + kept_task_count += 1 + lines.extend([ + f"### {kept_task_count}. {task['title']}", + task.get("description") or "No task description provided.", + "", + curated_text, + "" + ]) + + if excluded_tasks or report_exclusions: + lines.extend(["## Excluded Content", ""]) + for excluded in excluded_tasks: + lines.append(f"- Excluded task output: {excluded['title']} ({'; '.join(excluded['reasons'])})") + for excluded_line in list(dict.fromkeys(report_exclusions))[:10]: + if excluded_line: + lines.append(f"- {excluded_line}") + lines.append("") + + # Final Conclusion Generation + conclusion = ( + "Based on the approved task outputs, the project has successfully established a foundational framework. " + "The key findings suggest a viable path forward by focusing on the identified entry wedge and " + "mitigating primary risks through phased execution." + ) + + if variant == "full": + try: + # Use the 'Brief Writer' or any available agent to summarize a conclusion + agent_data = self._select_report_agent(project, "brief") + if agent_data: + agent = AgentFactory.get_agent( + provider=agent_data["api_provider"], + name=agent_data["name"], + role=agent_data["role"], + model=agent_data["model"], + system_prompt="You write a 2-3 sentence strategic conclusion and 3 actionable next steps for a project report. Never introduce placeholders or unsupported facts." + ) + report_so_far = "\n".join(lines) + res = await agent.run(f"Based on this project report, write a final strategic conclusion and 3 next steps:\n\n{report_so_far}", []) + if res.get("status") != "error": + data = res.get("data") + if isinstance(data, str): + conclusion = data + elif isinstance(data, dict): + conclusion = _format_conclusion_payload(data) + except Exception as exc: + logger.warning(f"Failed to generate dynamic conclusion: {exc}") + + lines.extend([ + "## Strategic Conclusion", + conclusion, + "", + "## Completion Status", + f"{len(tasks)} tasks reached done status. {kept_task_count} task outputs were included in the final report. {len(excluded_tasks)} task outputs were excluded from the final report." + ]) + + supabase.table("projects").update({"status": "completed"}).eq("id", project_id).execute() + report = "\n".join(lines) + + if variant != "full": + try: + generated = await self._generate_report_variant_with_agent(project, report, variant) + report = generated or self._build_fallback_variant(project, tasks, variant) or report + except Exception as exc: + logger.warning(f"Report variant generation failed: {exc}") + report = self._build_fallback_variant(project, tasks, variant) or report + + return { + "project_id": project_id, + "project_name": project["name"], + "task_count": kept_task_count, + "variant": variant, + "report": clean_report_text(dedupe_lines(report)), + "charts": _build_report_charts(curated_tasks) + } + + async def decompose_project(self, project_id: str): + """ + Uses a Planner agent to decompose a project into discrete tasks. + """ + project = supabase.table("projects").select("*").eq("id", project_id).single().execute().data + owner_id = project.get("owner_id") + + # Find a Planner agent, prioritizing Groq as requested + agents = supabase.table("agents").select("*").execute().data or [] + + # 1. Try to find an existing Groq Planner + planner_agent_data = next( + (a for a in agents if "Planner" in a["name"] and a.get("api_provider") == "groq"), + None + ) + + # 2. If not found, try any Planner + if not planner_agent_data: + planner_agent_data = next( + (a for a in agents if "Planner" in a["name"] and a.get("user_id") in (None, owner_id)), + next((a for a in agents if a.get("user_id") in (None, owner_id)), None) + ) + + # 3. If still no agent, or it's OpenAI but we want Groq, create a temporary one + if not planner_agent_data or (planner_agent_data.get("api_provider") == "openai" and not settings.OPENAI_API_KEY): + logger.info("Using default Groq Planner for decomposition.") + planner = AgentFactory.get_agent( + provider="groq", + name="System Planner", + role="Project Decomposer", + model="llama-3.3-70b-versatile", + system_prompt="You decompose goals into clear, ordered implementation tasks." + ) + else: + planner = AgentFactory.get_agent( + provider=planner_agent_data["api_provider"], + name=planner_agent_data["name"], + role=planner_agent_data["role"], + model=planner_agent_data["model"], + system_prompt=planner_agent_data.get("system_prompt") + ) + + prompt = f"""Decompose the following project into 3-5 clear, actionable implementation tasks. +Project Name: {project['name']} +Description: {project['description']} +Context: {project.get('context', 'None')} + +### Output Requirements: +You MUST return a valid JSON array of objects. Each object represents a task. +Do not include any conversational text, markdown formatting outside of the JSON, or explanations. + +### JSON Schema: +[ + {{ + "title": "string (The name of the task)", + "description": "string (Detailed instructions for the agent)", + "priority": "integer (1-5, where 5 is highest priority)" + }} +] + +IMPORTANT: Return a flat array. Do not wrap it in a parent 'tasks' object. +Do not use placeholder names or generic filler tasks. Every task title must be concrete and directly relevant to the stated project. +""" + + try: + result = await planner.run(prompt, []) + tasks_data = result.get("data") + + # Handle common LLM wrapping patterns + if isinstance(tasks_data, dict): + if "tasks" in tasks_data and isinstance(tasks_data["tasks"], list): + tasks_data = tasks_data["tasks"] + else: + tasks_data = [tasks_data] + + if not isinstance(tasks_data, list): + raise ValueError(f"Agent returned invalid format: {type(tasks_data)}. Expected list or dict.") + + # Filter out invalid tasks + valid_tasks = [ + t for t in tasks_data + if isinstance(t, dict) and t.get("title") + ] + + if not valid_tasks: + raise ValueError("No valid tasks extracted from agent output.") + + # Insert tasks + from .project_service import project_service + await project_service.add_tasks_to_project(project_id, valid_tasks) + logger.info(f"Auto-decomposed project {project_id} into {len(valid_tasks)} tasks.") + except Exception as e: + logger.error(f"Project decomposition failed: {e}") + + def _resolve_agent(self, task: dict, available_agents: list[dict]): + assigned_agent_id = task.get("assigned_agent_id") + if assigned_agent_id: + return next((agent for agent in available_agents if agent["id"] == assigned_agent_id), None) + return available_agents[0] if available_agents else None + + async def _run_task(self, task: dict, agent_data: dict): + await AgentRunnerService.run_agent_task( + task, + agent_data, + start_action="orchestrator_execution_start", + start_content=f"Orchestrator assigned {agent_data['name']} to task: {task['title']}", + complete_action="orchestrator_execution_complete", + complete_content="Task completed and is awaiting approval." + ) + +orchestrator_service = OrchestratorService() diff --git a/backend/services/output_quality.py b/backend/services/output_quality.py new file mode 100644 index 0000000000000000000000000000000000000000..a2e495861bef6ab12117d892e173c648d76d633e --- /dev/null +++ b/backend/services/output_quality.py @@ -0,0 +1,312 @@ +import json +import re +from collections import OrderedDict +from typing import Any + +PLACEHOLDER_PATTERNS = [ + r"\bCompetitor\s+[A-Z]\b", + r"\bDashboard\s+[A-Z]\b", + r"\bProduct\s+[A-Z]\b", + r"\bCompany\s+[A-Z]\b", + r"\bOur Company\b", +] + +GENERIC_FILLER_PATTERNS = [ + r"\bsustainable products?\b", + r"\bdigital marketing\b", + r"\bcustomer segments?\b", + r"\bdemographics\b", + r"\bpsychographics\b", + r"\bdistribution channels?\b", +] + +SENSITIVE_FACT_PATTERNS = [ + r"\bmarket share\b", + r"\brevenue\b", + r"\barr\b", + r"\bpricing\b", + r"\bprice\b", + r"\blatest release version\b", + r"\bprofit\b", +] + +RAW_DUMP_PATTERNS = [ + r"```(?:json)?", + r'"raw_text"\s*:', + r'"projectoverview"\s*:', + r'"projectoverview"\s*:', + r'"userstories"\s*:', + r'"datamodel"\s*:', +] + +LATAM_HINTS = [ + "mercadolibre", + "mercado libre", + "latam", + "latin america", + "argentina", + "mexico", + "brazil", + "brasil", + "chile", + "colombia", + "peru", + "uruguay", +] + +SEA_HINTS = [ + "indonesia", + "yogyakarta", + "bali", + "southeast asia", + "tokopedia", + "shopee", + "jakarta", +] + +STRICT_TASK_PATTERNS = [ + r"\bresearch\b", + r"\banaly[sz]e\b", + r"\banalysis\b", + r"\bcompetitor\b", + r"\bpricing\b", + r"\bmarket\b", + r"\baudit\b", + r"\breport\b", + r"\bcompare\b", +] + + +def _stringify_payload(value: Any) -> str: + if value is None: + return "" + if isinstance(value, str): + return value + try: + return json.dumps(value, ensure_ascii=True) + except Exception: + return str(value) + + +def build_quality_instructions(task: dict) -> str: + project_text = _project_text(task) + task_text = f"{task.get('title', '')}\n{task.get('description', '')}\n{project_text}".lower() + strict_mode = any(re.search(pattern, task_text, re.IGNORECASE) for pattern in STRICT_TASK_PATTERNS) + + base = [ + "Output quality rules:", + "- Never use placeholder names like Competitor A, Dashboard B, Product C, or Our Company.", + "- If a real named entity cannot be identified with confidence, return unknown instead of inventing one.", + "- Keep the output strictly within the requested scope.", + "- Stay aligned with the project's stated geography, competitors, and market context. Do not switch regions or industries unless the task explicitly requires it.", + "- Do not include generic filler sections that were not requested.", + "- Use clean UTF-8/ASCII friendly text. Do not output corrupted characters.", + "- Do not return raw JSON dumps, code blocks, repository scaffolds, or intermediate planning artifacts unless the task explicitly asks for them.", + ] + + if strict_mode: + base.extend( + [ + "- Return structured JSON where possible.", + "- For factual claims about competitors, products, pricing, versions, revenue, market share, or benchmarks, include source_url when available.", + "- Do not invent pricing, release versions, market share, revenue, ARR impact, or benchmarks.", + "- If a sensitive fact cannot be verified, omit it or mark it unknown.", + ] + ) + + return "\n".join(base) + + +def _project_text(task: dict) -> str: + project = task.get("project") + if isinstance(project, dict): + return "\n".join( + str(project.get(key, "") or "") + for key in ("name", "description", "context") + ) + return str(task.get("project_context") or "") + + +def _contains_any(text: str, terms: list[str]) -> bool: + lowered = text.lower() + return any(term in lowered for term in terms) + + +def _looks_like_raw_dump(text: str) -> bool: + # Extremely relaxed check: Only flag as raw dump if it contains internal system keys + # that indicate it's a raw unformatted API response rather than a report. + internal_keys = [r'"raw_text"\s*:', r'"internal_status"\s*:', r'"debug_info"\s*:'] + if any(re.search(pattern, text, re.IGNORECASE) for pattern in internal_keys): + return True + + return False + + +def _is_context_drift(task_text: str, output_text: str) -> bool: + task_lower = task_text.lower() + output_lower = output_text.lower() + + if _contains_any(task_lower, LATAM_HINTS) and _contains_any(output_lower, SEA_HINTS): + return True + + return False + + +def validate_output(task: dict, result: dict) -> dict: + raw_text = _stringify_payload(result.get("raw_output")) + data_text = _stringify_payload(result.get("data")) + combined = "\n".join(part for part in [raw_text, data_text] if part).strip() + task_text = "\n".join( + [ + str(task.get("title", "") or ""), + str(task.get("description", "") or ""), + _project_text(task), + ] + ) + + fail_reasons: list[str] = [] + must_fix: list[str] = [] + placeholder_entities: list[str] = [] + unsupported_claims: list[str] = [] + duplicate_claims: list[str] = [] + encoding_issues: list[str] = [] + + if not combined: + fail_reasons.append("Empty output.") + + for pattern in PLACEHOLDER_PATTERNS: + matches = re.findall(pattern, combined, re.IGNORECASE) + placeholder_entities.extend(matches) + + if placeholder_entities: + # We don't add to fail_reasons anymore, just let the score reduction handle it + pass + + if "■" in combined: + encoding_issues.append("Found corrupted character '■'.") + + if encoding_issues: + fail_reasons.append("Output contains encoding corruption.") + must_fix.append("Remove corrupted characters and normalize text encoding.") + + if _looks_like_raw_dump(combined): + fail_reasons.append("Output contains raw JSON/code dump instead of a usable task result.") + must_fix.append("Convert intermediate JSON/code output into the requested final artifact.") + + if _is_context_drift(task_text, combined): + fail_reasons.append("Output drifted away from the project's stated geography or market context.") + must_fix.append("Regenerate the output using the project's explicit region, competitor set, and business context.") + + for pattern in GENERIC_FILLER_PATTERNS: + if re.search(pattern, combined, re.IGNORECASE): + unsupported_claims.append(pattern.replace("\\b", "").replace("?", "")) + + if unsupported_claims: + fail_reasons.append("Output contains generic filler outside the likely project scope.") + must_fix.append("Remove generic business-analysis filler not tied to the requested task.") + + has_source_url = bool(re.search(r"https?://", combined, re.IGNORECASE)) + for pattern in SENSITIVE_FACT_PATTERNS: + if re.search(pattern, combined, re.IGNORECASE) and not has_source_url: + unsupported_claims.append(f"Sensitive fact without source: {pattern}") + + if any(item.startswith("Sensitive fact without source:") for item in unsupported_claims): + # We don't add to fail_reasons anymore, just let the score reduction handle it + pass + + normalized_lines = [] + seen_lines: set[str] = set() + for line in combined.splitlines(): + normalized = re.sub(r"\s+", " ", line).strip().lower() + if len(normalized) < 20: + continue + if normalized in seen_lines: + duplicate_claims.append(line.strip()) + else: + seen_lines.add(normalized) + normalized_lines.append(normalized) + + if duplicate_claims: + # Just let the score reduction handle it + pass + + score = 100 + if placeholder_entities: + score = min(score, 20) + if _looks_like_raw_dump(combined): + score = min(score, 20) + if _is_context_drift(task_text, combined): + score = min(score, 20) + if any(item.startswith("Sensitive fact without source:") for item in unsupported_claims): + score = min(score, 30) + if duplicate_claims: + score = min(score, 50) + if unsupported_claims and not any(item.startswith("Sensitive fact without source:") for item in unsupported_claims): + score = min(score, 60) + if encoding_issues: + score = min(score, 60) + if not combined: + score = 0 + + approved = score >= 20 + return { + "approved": approved, + "score": score, + "fail_reasons": fail_reasons, + "must_fix": must_fix, + "duplicate_claims": list(OrderedDict.fromkeys(duplicate_claims))[:10], + "unsupported_claims": list(OrderedDict.fromkeys(unsupported_claims))[:10], + "placeholder_entities": list(OrderedDict.fromkeys(placeholder_entities))[:10], + "encoding_issues": encoding_issues, + } + + +def report_text_from_output(output_data: Any) -> str: + if not output_data: + return "" + if isinstance(output_data, dict): + primary = output_data.get("data") or output_data.get("final") or output_data.get("raw_output") or output_data + else: + primary = output_data + return _stringify_payload(primary) + + +def clean_report_text(text: str) -> str: + cleaned = text.replace("■", "-").replace("\u25A0", "-") + cleaned = re.sub(r"[ \t]+", " ", cleaned) + cleaned = re.sub(r"\n{3,}", "\n\n", cleaned) + return cleaned.strip() + + +def dedupe_lines(text: str) -> str: + lines = text.splitlines() + kept: list[str] = [] + seen: set[str] = set() + for line in lines: + normalized = re.sub(r"\s+", " ", line).strip().lower() + if normalized and len(normalized) > 15 and normalized in seen: + continue + if normalized: + seen.add(normalized) + kept.append(line) + return "\n".join(kept).strip() + + +def filter_report_sections(text: str) -> tuple[str, list[str]]: + excluded: list[str] = [] + kept_lines: list[str] = [] + for line in text.splitlines(): + lowered = line.lower() + if any(re.search(pattern, lowered, re.IGNORECASE) for pattern in PLACEHOLDER_PATTERNS): + excluded.append("Removed placeholder content.") + continue + if any(re.search(pattern, lowered, re.IGNORECASE) for pattern in GENERIC_FILLER_PATTERNS): + excluded.append("Removed generic filler outside the requested scope.") + continue + if _looks_like_raw_dump(line): + excluded.append("Removed raw JSON/code dump content.") + continue + kept_lines.append(line) + return "\n".join(kept_lines).strip(), excluded + + diff --git a/backend/services/project_service.py b/backend/services/project_service.py new file mode 100644 index 0000000000000000000000000000000000000000..0929a390aa8e595779553dd4d48d20c78059f885 --- /dev/null +++ b/backend/services/project_service.py @@ -0,0 +1,35 @@ +from services.supabase_service import supabase +from typing import List, Dict, Any +import logging + +logger = logging.getLogger("uvicorn") + +class ProjectService: + """ + Handles the creation and management of projects and their constituent tasks. + """ + + @staticmethod + async def create_project(title: str, description: str, user_id: str) -> Dict[str, Any]: + res = supabase.table("projects").insert({ + "title": title, + "description": description, + "user_id": user_id, + "status": "active" + }).execute() + return res.data[0] + + @staticmethod + async def add_tasks_to_project(project_id: str, tasks: List[Dict[str, Any]]): + """ + Adds a list of tasks to a project. + tasks: [{"title": "...", "description": "...", "assigned_agent_id": "..."}] + """ + formatted_tasks = [ + {**task, "project_id": project_id, "status": "todo"} + for task in tasks + ] + supabase.table("tasks").insert(formatted_tasks).execute() + logger.info(f"Added {len(tasks)} tasks to project {project_id}") + +project_service = ProjectService() diff --git a/backend/services/semantic_backprop.py b/backend/services/semantic_backprop.py new file mode 100644 index 0000000000000000000000000000000000000000..d163022dada1282fda5cb9b230c7f37dc94ce221 --- /dev/null +++ b/backend/services/semantic_backprop.py @@ -0,0 +1,104 @@ +import re +import logging +from typing import List, Dict, Any +from services.supabase_service import supabase + +logger = logging.getLogger("uvicorn") + +class SemanticBackpropService: + """ + Ensures numerical consistency across agent tasks by extracting 'Canonical Numbers' + from previous task outputs. + """ + + @staticmethod + async def get_project_context(project_id: str, current_task_id: str) -> str: + """ + Fetches and extracts canonical figures from all completed sibling tasks. + """ + try: + resp = supabase.table("tasks") \ + .select("title, output_data") \ + .eq("project_id", project_id) \ + .eq("status", "done") \ + .neq("id", current_task_id) \ + .execute() + + if not resp.data: + return "" + + canonical_blocks = [] + topic_blocks = [] + + for task in resp.data: + output = task.get("output_data") or {} + # Handle different output formats (raw string or dict with 'result') + result_text = "" + if isinstance(output, dict): + result_text = output.get("result", "") or output.get("raw_output", "") + elif isinstance(output, str): + result_text = output + + if not result_text: + continue + + # Extract financial and numerical lines + lines = result_text.splitlines() + financial_lines = [] + + # Keywords that often indicate a 'canonical' number + keywords = [ + "$", "%", "USD", "MRR", "ARR", "ROI", "cost", "budget", + "revenue", "price", "fee", "estimate", "total", "quota" + ] + + for line in lines: + if any(k.lower() in line.lower() for k in keywords): + if len(line.strip()) > 5: # Ignore very short lines + financial_lines.append(line.strip()) + + if financial_lines: + # De-duplicate similar lines + seen = set() + unique_fin = [] + for fl in financial_lines: + key = fl[:50] + if key not in seen: + seen.add(key) + unique_fin.append(fl) + + canonical_blocks.append( + f"Source Task: **{task['title']}**\n" + + "\n".join(f" • {fl}" for fl in unique_fin[:8]) + ) + + # Also track what topics were covered to avoid repetition + topic_blocks.append(f"- **{task['title']}**: (Covered in previous step)") + + if not canonical_blocks and not topic_blocks: + return "" + + context = "\n---\n" + if canonical_blocks: + context += ( + "### ⚠️ CANONICAL FIGURES — PREVIOUSLY ESTABLISHED\n" + "> **MANDATORY RULE**: The following numbers and figures were established by agents\n" + "> responsible for those domains. You MUST use these exact values if you reference them.\n" + "> DO NOT re-calculate or propose alternative values for these specific items.\n\n" + ) + context += "\n\n".join(canonical_blocks) + "\n\n" + + if topic_blocks: + context += ( + "### 📋 PREVIOUSLY COVERED TOPICS\n" + "> Do not repeat the analysis of these topics. Focus only on your specific task.\n" + ) + context += "\n".join(topic_blocks) + "\n" + + return context + + except Exception as e: + logger.error(f"Semantic Backprop failed: {e}") + return "" + +semantic_backprop = SemanticBackpropService() diff --git a/backend/services/supabase_service.py b/backend/services/supabase_service.py new file mode 100644 index 0000000000000000000000000000000000000000..378a01c5097d7337ed904640816338a6a4e51ee4 --- /dev/null +++ b/backend/services/supabase_service.py @@ -0,0 +1,13 @@ +from supabase import create_client, Client +from services.config import settings + +def get_supabase_client() -> Client: + """ + Initializes and returns a Supabase client. + """ + if not settings.SUPABASE_URL or not settings.SUPABASE_SERVICE_ROLE_KEY: + raise ValueError("SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY must be set in environment.") + + return create_client(settings.SUPABASE_URL, settings.SUPABASE_SERVICE_ROLE_KEY) + +supabase: Client = get_supabase_client() diff --git a/backend/services/task_queue.py b/backend/services/task_queue.py new file mode 100644 index 0000000000000000000000000000000000000000..504fc0fec0e2d8e094ce791fc787268e67d3eeac --- /dev/null +++ b/backend/services/task_queue.py @@ -0,0 +1,47 @@ +import logging +from typing import Optional, List +from .supabase_service import supabase + +logger = logging.getLogger(__name__) + +class TaskQueueService: + @staticmethod + async def queue_task(task_id: str): + """ + Marks a task as 'queued' in the database. + """ + try: + result = supabase.table("tasks").update({"status": "queued"}).eq("id", task_id).execute() + return result + except Exception as e: + logger.error(f"Error queueing task {task_id}: {e}") + return None + + @staticmethod + async def get_next_queued_task(): + """ + Fetches the next available task from the queue. + """ + try: + # Fetch one task that is in 'queued' status, ordered by priority and created_at + result = supabase.table("tasks") \ + .select("*") \ + .eq("status", "queued") \ + .order("priority", desc=True) \ + .order("created_at") \ + .limit(1) \ + .execute() + + if result.data: + return result.data[0] + return None + except Exception as e: + logger.error(f"Error fetching next queued task: {e}") + return None + + @staticmethod + async def mark_in_progress(task_id: str): + """ + Marks a task as 'in_progress'. + """ + return supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute() diff --git a/backend/tools/browser.py b/backend/tools/browser.py new file mode 100644 index 0000000000000000000000000000000000000000..d31153a8d1c09a846fce2685838efc8ef5fe71b4 --- /dev/null +++ b/backend/tools/browser.py @@ -0,0 +1,111 @@ +import logging +from typing import Any + +import httpx +from playwright.async_api import async_playwright + +from services.config import settings + +logger = logging.getLogger("uvicorn") + + +class BrowserTool: + """ + Tools for live web search and direct URL extraction. + """ + + def __init__(self) -> None: + self.tavily_api_key = settings.TAVILY_API_KEY + + async def search_and_extract(self, url: str) -> str: + """ + Navigates to a URL and returns the page text content. + """ + logger.info("BrowserTool: Navigating to %s", url) + async with async_playwright() as playwright: + browser = await playwright.chromium.launch(headless=True) + page = await browser.new_page() + try: + await page.goto(url, wait_until="networkidle", timeout=30000) + title = await page.title() + content = await page.inner_text("body") + combined = f"Title: {title}\nURL: {url}\n\n{content}".strip() + return combined[:12000] + except Exception as exc: + logger.error("BrowserTool extract error for %s: %s", url, exc) + return f"Error accessing {url}: {exc}" + finally: + await browser.close() + + async def web_search(self, query: str, topic: str = "general", max_results: int = 5) -> str: + """ + Searches the public web with Tavily and returns LLM-friendly results. + """ + if not self.tavily_api_key: + return ( + "Web search is unavailable: TAVILY_API_KEY is not configured. " + "Add it to the backend environment to enable internet search." + ) + + payload = { + "query": query, + "topic": topic if topic in {"general", "news", "finance"} else "general", + "search_depth": "advanced", + "max_results": max(1, min(max_results, 10)), + "include_answer": "advanced", + "include_raw_content": False, + "include_images": False, + } + + headers = { + "Authorization": f"Bearer {self.tavily_api_key}", + "Content-Type": "application/json", + } + + try: + async with httpx.AsyncClient(timeout=45.0) as client: + response = await client.post( + "https://api.tavily.com/search", + headers=headers, + json=payload, + ) + response.raise_for_status() + except httpx.HTTPStatusError as exc: + detail = exc.response.text[:500] if exc.response is not None else str(exc) + logger.error("Tavily HTTP error: %s", detail) + return f"Tavily search failed with status {exc.response.status_code}: {detail}" + except Exception as exc: + logger.error("Tavily request error: %s", exc) + return f"Tavily search failed: {exc}" + + data = response.json() + return self._format_tavily_results(query, data) + + def _format_tavily_results(self, query: str, data: dict[str, Any]) -> str: + answer = data.get("answer") + results = data.get("results") or [] + + lines = [f"Search query: {query}"] + if answer: + lines.extend(["", "Answer:", str(answer).strip()]) + + if not results: + lines.extend(["", "No search results returned."]) + return "\n".join(lines) + + lines.extend(["", "Sources:"]) + for index, result in enumerate(results, start=1): + title = result.get("title") or "Untitled" + url = result.get("url") or "" + snippet = (result.get("content") or "").strip() + score = result.get("score") + + lines.append(f"{index}. {title}") + if url: + lines.append(f" URL: {url}") + if score is not None: + lines.append(f" Score: {score}") + if snippet: + lines.append(f" Snippet: {snippet[:900]}") + + return "\n".join(lines)[:12000] diff --git a/backend/tools/decomposer.py b/backend/tools/decomposer.py new file mode 100644 index 0000000000000000000000000000000000000000..6f568eab896f881815afe79a6f3d685ba24d3107 --- /dev/null +++ b/backend/tools/decomposer.py @@ -0,0 +1,20 @@ +from services.project_service import project_service +from typing import List, Dict, Any +import logging + +logger = logging.getLogger("uvicorn") + +class DecompositionTool: + """ + A tool that allows agents to break down complex goals into actionable tasks. + """ + async def create_subtasks(self, project_id: str, tasks: List[Dict[str, Any]]) -> str: + """ + Takes a list of task definitions and adds them to the database for the given project. + """ + logger.info(f"DecompositionTool: Creating {len(tasks)} subtasks for project {project_id}") + try: + await project_service.add_tasks_to_project(project_id, tasks) + return f"Successfully created {len(tasks)} subtasks." + except Exception as e: + return f"Failed to create subtasks: {str(e)}" diff --git a/backend/tools/file_generator.py b/backend/tools/file_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..ad12b2ed3fe44ba7a601ddc47dda9b4d7a51eca3 --- /dev/null +++ b/backend/tools/file_generator.py @@ -0,0 +1,61 @@ +import os +import pandas as pd +from reportlab.lib.pagesizes import letter +from reportlab.pdfgen import canvas +from datetime import datetime +import logging + +logger = logging.getLogger("uvicorn") + +class FileGeneratorTool: + """ + A tool that allows agents to generate PDF and Excel files. + """ + def __init__(self): + self.output_dir = "outputs" + os.makedirs(self.output_dir, exist_ok=True) + + def _generate_filename(self, extension: str) -> str: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + return os.path.join(self.output_dir, f"report_{timestamp}.{extension}") + + async def generate_pdf(self, title: str, content: str) -> str: + """ + Generates a PDF document with the provided title and content. + """ + filename = self._generate_filename("pdf") + logger.info(f"FileGenerator: Generating PDF {filename}") + + try: + c = canvas.Canvas(filename, pagesize=letter) + width, height = letter + + # Title + c.setFont("Helvetica-Bold", 16) + c.drawString(72, height - 72, title) + + # Content (very basic wrapping/split) + c.setFont("Helvetica", 12) + text_object = c.beginText(72, height - 100) + for line in content.split('\n'): + text_object.textLine(line) + c.drawText(text_object) + + c.save() + return f"PDF generated successfully: {filename}" + except Exception as e: + return f"Failed to generate PDF: {str(e)}" + + async def generate_excel(self, data: list) -> str: + """ + Generates an Excel file from a list of dictionaries. + """ + filename = self._generate_filename("xlsx") + logger.info(f"FileGenerator: Generating Excel {filename}") + + try: + df = pd.DataFrame(data) + df.to_excel(filename, index=False) + return f"Excel generated successfully: {filename}" + except Exception as e: + return f"Failed to generate Excel: {str(e)}" diff --git a/backend/tools/registry.py b/backend/tools/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..952d3999bfd2dfaec213b79aa845456690c6819c --- /dev/null +++ b/backend/tools/registry.py @@ -0,0 +1,260 @@ +from .file_generator import FileGeneratorTool +from .decomposer import DecompositionTool +from .sre import SRETool +from .browser import BrowserTool +from .sandbox import CodeSandboxTool +from .visuals import VisualsTool +from typing import Any, Dict, List + +class ToolRegistry: + def __init__(self): + self.browser = BrowserTool() + self.sandbox = CodeSandboxTool() + self.file_gen = FileGeneratorTool() + self.decomposer = DecompositionTool() + self.sre = SRETool() + self.visuals = VisualsTool() + self.tools = { + "web_search": { + "func": self.browser.web_search, + "description": "Searches the public web using Tavily and returns summarized results with source URLs." + }, + "extract_url": { + "func": self.browser.search_and_extract, + "description": "Extracts text content from a specific URL." + }, + "execute_python": { + "func": self.sandbox.execute_python, + "description": "Executes Python code and returns the output." + }, + "generate_pdf": { + "func": self.file_gen.generate_pdf, + "description": "Generates a PDF document." + }, + "generate_excel": { + "func": self.file_gen.generate_excel, + "description": "Generates an Excel spreadsheet from structured data." + }, + "create_subtasks": { + "func": self.decomposer.create_subtasks, + "description": "Breaks down a goal into a list of actionable tasks." + }, + "generate_chart": { + "func": self.visuals.generate_chart, + "description": "Generates a chart image (bar, line, pie) from a JSON config." + }, + "generate_illustration": { + "func": self.visuals.generate_illustration, + "description": "Generates an AI illustration or drawing based on a text prompt." + }, + "get_system_health": { + "func": self.sre.get_system_health, + "description": "Returns system health metrics (CPU, Memory, Disk)." + }, + "check_service_status": { + "func": self.sre.check_service_status, + "description": "Checks if a specific service or process is running." + }, + "run_patch_command": { + "func": self.sre.run_patch_command, + "description": "Executes a safe system patch command (e.g., git pull, npm install)." + } + } + + def get_tool_definitions(self) -> List[Dict[str, Any]]: + """ + Returns OpenAI-style tool definitions. + """ + return [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the public web for information using Tavily. Use this when the task requires current external information.", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "The search query"}, + "topic": { + "type": "string", + "enum": ["general", "news", "finance"], + "description": "The search category. Use news for recent events and finance for market/company financial queries." + }, + "max_results": { + "type": "integer", + "description": "Maximum number of results to return. Keep this small to control context size.", + "default": 5 + } + }, + "required": ["query"] + } + } + }, + { + "type": "function", + "function": { + "name": "extract_url", + "description": "Extract text content from a URL", + "parameters": { + "type": "object", + "properties": { + "url": {"type": "string", "description": "The URL to extract from"} + }, + "required": ["url"] + } + } + }, + { + "type": "function", + "function": { + "name": "execute_python", + "description": "Execute Python code to perform calculations, data analysis, or logic verification.", + "parameters": { + "type": "object", + "properties": { + "code": {"type": "string", "description": "The Python code to execute"} + }, + "required": ["code"] + } + } + }, + { + "type": "function", + "function": { + "name": "generate_pdf", + "description": "Create a professional PDF report", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "The title of the report"}, + "content": {"type": "string", "description": "The text content of the report"} + }, + "required": ["title", "content"] + } + } + }, + { + "type": "function", + "function": { + "name": "generate_excel", + "description": "Create an Excel spreadsheet from data", + "parameters": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": {"type": "object"}, + "description": "List of rows as objects" + } + }, + "required": ["data"] + } + } + }, + { + "type": "function", + "function": { + "name": "create_subtasks", + "description": "Break down a complex goal into smaller, actionable tasks for other agents.", + "parameters": { + "type": "object", + "properties": { + "project_id": {"type": "string", "description": "The current project UUID"}, + "tasks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Clear title of the subtask"}, + "description": {"type": "string", "description": "Detailed instructions for the next agent"}, + "assigned_agent_id": {"type": "string", "description": "The UUID of the agent to handle this task"} + }, + "required": ["title", "description", "assigned_agent_id"] + } + } + }, + "required": ["project_id", "tasks"] + } + } + }, + { + "type": "function", + "function": { + "name": "generate_chart", + "description": "Generate a visual chart image (bar, line, pie, etc.) using QuickChart.io.", + "parameters": { + "type": "object", + "properties": { + "chart_type": {"type": "string", "enum": ["bar", "line", "pie", "doughnut"], "description": "Type of chart"}, + "chart_config": {"type": "string", "description": "The JSON configuration for QuickChart (e.g., {type: 'bar', data: {...}})"} + }, + "required": ["chart_type", "chart_config"] + } + } + }, + { + "type": "function", + "function": { + "name": "generate_illustration", + "description": "Generate an AI illustration or drawing based on a prompt using Pollinations.ai.", + "parameters": { + "type": "object", + "properties": { + "prompt": {"type": "string", "description": "Detailed description of the illustration to generate"} + }, + "required": ["prompt"] + } + } + }, + { + "type": "function", + "function": { + "name": "get_system_health", + "description": "Monitor server vital signs like CPU usage, memory availability, and disk space.", + "parameters": { + "type": "object", + "properties": {} + } + } + }, + { + "type": "function", + "function": { + "name": "check_service_status", + "description": "Verify if a critical service or process is currently active on the host.", + "parameters": { + "type": "object", + "properties": { + "service_name": {"type": "string", "description": "The name of the process or service to check"} + }, + "required": ["service_name"] + } + } + }, + { + "type": "function", + "function": { + "name": "run_patch_command", + "description": "Apply a system patch or update. Limited to safe commands like 'git pull' or 'npm install'.", + "parameters": { + "type": "object", + "properties": { + "command": {"type": "string", "description": "The restricted command to execute"} + }, + "required": ["command"] + } + } + } + ] + + async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any: + """ + Executes a tool by name with provided arguments. + """ + if name not in self.tools: + raise ValueError(f"Tool {name} not found") + + func = self.tools[name]["func"] + return await func(**arguments) + +tool_registry = ToolRegistry() diff --git a/backend/tools/sandbox.py b/backend/tools/sandbox.py new file mode 100644 index 0000000000000000000000000000000000000000..dc8d5ec1290e8c71aa23ad261ddf56248cfb3309 --- /dev/null +++ b/backend/tools/sandbox.py @@ -0,0 +1,39 @@ +import sys +import io +import contextlib +import logging + +logger = logging.getLogger("uvicorn") + +class CodeSandboxTool: + """ + A tool that allows agents to execute Python code and see the output. + """ + async def execute_python(self, code: str) -> str: + """ + Executes the provided Python code and returns the stdout/stderr. + """ + logger.info("CodeSandboxTool: Executing Python code") + + # Capture stdout and stderr + stdout = io.StringIO() + stderr = io.StringIO() + + try: + with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + # Using a fresh globals dictionary for each execution + exec_globals = {} + exec(code, exec_globals) + + output = stdout.getvalue() + errors = stderr.getvalue() + + if errors: + return f"Output:\n{output}\nErrors:\n{errors}" + return output if output else "Execution successful (no output)." + + except Exception as e: + return f"Execution failed: {str(e)}" + finally: + stdout.close() + stderr.close() diff --git a/backend/tools/sre.py b/backend/tools/sre.py new file mode 100644 index 0000000000000000000000000000000000000000..3a040630b4d844903d8f24cf3abf5fe2b2a8d39f --- /dev/null +++ b/backend/tools/sre.py @@ -0,0 +1,72 @@ +import psutil +import os +import platform +from typing import Dict, Any +import logging + +logger = logging.getLogger("uvicorn") + +class SRETool: + """ + A toolset for Site Reliability Engineering (SRE) agents to monitor and manage system health. + """ + + async def get_system_health(self) -> Dict[str, Any]: + """ + Returns real-time system health metrics (CPU, RAM, Disk). + """ + logger.info("SRETool: Gathering system health metrics") + return { + "cpu_percent": psutil.cpu_percent(interval=1), + "memory": { + "total": psutil.virtual_memory().total, + "available": psutil.virtual_memory().available, + "percent": psutil.virtual_memory().percent + }, + "disk": { + "total": psutil.disk_usage('/').total, + "used": psutil.disk_usage('/').used, + "percent": psutil.disk_usage('/').percent + }, + "os": platform.system(), + "uptime": self._get_uptime() + } + + async def check_service_status(self, service_name: str) -> str: + """ + Checks if a specific service/process is running. + """ + logger.info(f"SRETool: Checking status of {service_name}") + for proc in psutil.process_iter(['name']): + if service_name.lower() in proc.info['name'].lower(): + return f"Service '{service_name}' is RUNNING." + return f"Service '{service_name}' is NOT running." + + def _get_uptime(self) -> str: + # Simple uptime calculation + import time + boot_time = psutil.boot_time() + uptime_seconds = time.time() - boot_time + return f"{int(uptime_seconds // 3600)}h {int((uptime_seconds % 3600) // 60)}m" + + async def run_patch_command(self, command: str) -> str: + """ + Executes a restricted set of patching commands. + """ + logger.warning(f"SRETool: Attempting to run patch command: {command}") + + # Restricted whitelist for security + whitelist = ["npm install", "pip install", "git pull", "npm audit fix"] + + is_safe = any(command.startswith(safe) for safe in whitelist) + if not is_safe: + return f"Command '{command}' is not in the safety whitelist. Patch rejected." + + try: + import subprocess + result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60) + if result.returncode == 0: + return f"Patch successful: {result.stdout}" + return f"Patch failed: {result.stderr}" + except Exception as e: + return f"Error executing patch: {str(e)}" diff --git a/backend/tools/visuals.py b/backend/tools/visuals.py new file mode 100644 index 0000000000000000000000000000000000000000..9813d40a3f53026b393294a2ceae879ba946096d --- /dev/null +++ b/backend/tools/visuals.py @@ -0,0 +1,48 @@ +import urllib.parse +import json +import logging + +logger = logging.getLogger("uvicorn") + +class VisualsTool: + """ + Provides visual generation capabilities like charts and illustrations. + """ + + async def generate_chart(self, chart_type: str, chart_config: str) -> str: + """ + Generates a chart using QuickChart.io. + chart_type: 'bar', 'line', 'pie', 'doughnut' + chart_config: A JSON string containing the QuickChart configuration. + """ + try: + # If chart_config is already a dict, convert to JSON + if isinstance(chart_config, dict): + config_str = json.dumps(chart_config) + else: + # Try to parse it to validate + config_str = chart_config + json.loads(config_str) + + encoded_config = urllib.parse.quote(config_str) + url = f"https://quickchart.io/chart?c={encoded_config}" + + logger.info(f"Generated chart URL: {url}") + return f"Chart generated successfully: {url}. You should include this URL in your markdown output as an image: ![Chart]({url})" + except Exception as e: + logger.error(f"Failed to generate chart: {e}") + return f"Error generating chart: {str(e)}. Please ensure your chart_config is a valid JSON string." + + async def generate_illustration(self, prompt: str) -> str: + """ + Generates an illustration using Pollinations.ai. + """ + try: + encoded_prompt = urllib.parse.quote(prompt) + url = f"https://pollinations.ai/p/{encoded_prompt}?width=1024&height=1024&seed=42&model=flux" + + logger.info(f"Generated illustration URL: {url}") + return f"Illustration generated successfully: {url}. You should include this URL in your markdown output as an image: ![Illustration]({url})" + except Exception as e: + logger.error(f"Failed to generate illustration: {e}") + return f"Error generating illustration: {str(e)}" diff --git a/backend/worker.py b/backend/worker.py new file mode 100644 index 0000000000000000000000000000000000000000..f8dfa01f27222447fbd48a87857303e1ce29e7cf --- /dev/null +++ b/backend/worker.py @@ -0,0 +1,53 @@ +import asyncio +import logging +import signal +from services.task_queue import TaskQueueService +from services.supabase_service import supabase +from services.agent_runner_service import AgentRunnerService + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("worker") + +class AubmWorker: + def __init__(self): + self.running = True + + async def start(self): + logger.info("Aubm Background Worker started.") + while self.running: + task = await TaskQueueService.get_next_queued_task() + + if task: + task_id = task['id'] + logger.info(f"Processing task: {task_id}") + + try: + # Fetch agent data for this task + agent_res = supabase.table("agents").select("*").eq("id", task["assigned_agent_id"]).single().execute() + if agent_res.data: + await AgentRunnerService.execute_agent_logic(task, agent_res.data) + logger.info(f"Task {task_id} completed successfully.") + else: + logger.error(f"No agent found for task {task_id}") + except Exception as e: + logger.error(f"Failed to process task {task_id}: {e}") + else: + # No tasks, sleep for a bit + await asyncio.sleep(5) + + def stop(self): + logger.info("Stopping worker...") + self.running = False + +async def main(): + worker = AubmWorker() + + # Handle shutdown signals + loop = asyncio.get_running_loop() + for sig in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler(sig, worker.stop) + + await worker.start() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/database/agent_ownership.sql b/database/agent_ownership.sql new file mode 100644 index 0000000000000000000000000000000000000000..ee813b241b42d6ed1722afa996c44a491e1ad5a4 --- /dev/null +++ b/database/agent_ownership.sql @@ -0,0 +1,42 @@ +-- Agent ownership and marketplace deploy policies +-- Apply this migration to existing Supabase projects after schema.sql. + +ALTER TABLE public.agents +ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users ON DELETE CASCADE; + +ALTER TABLE public.agents ENABLE ROW LEVEL SECURITY; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'agents' + AND policyname = 'Users can create own agents' + ) THEN + CREATE POLICY "Users can create own agents" ON public.agents + FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'agents' + AND policyname = 'Users can update own agents' + ) THEN + CREATE POLICY "Users can update own agents" ON public.agents + FOR UPDATE TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'agents' + AND policyname = 'Users can delete own agents' + ) THEN + CREATE POLICY "Users can delete own agents" ON public.agents + FOR DELETE TO authenticated USING (auth.uid() = user_id); + END IF; +END $$; + +NOTIFY pgrst, 'reload schema'; diff --git a/database/default_agents.sql b/database/default_agents.sql new file mode 100644 index 0000000000000000000000000000000000000000..104a78e8348ff2b2668f4d9c40875ffe13b5e90c --- /dev/null +++ b/database/default_agents.sql @@ -0,0 +1,36 @@ +-- Global default agents +-- These agents are readable by authenticated users and usable by the backend +-- orchestrator as fallback agents. They are not owned by a specific user. + +INSERT INTO public.agents (name, role, api_provider, model, system_prompt) +SELECT + 'Planner', + 'Project Planner', + 'openai', + 'gpt-4o', + 'You decompose goals into clear, ordered implementation tasks.' +WHERE NOT EXISTS ( + SELECT 1 FROM public.agents WHERE user_id IS NULL AND name = 'Planner' +); + +INSERT INTO public.agents (name, role, api_provider, model, system_prompt) +SELECT + 'Builder', + 'Implementation Agent', + 'openai', + 'gpt-4o', + 'You implement practical, production-oriented solutions with concise output.' +WHERE NOT EXISTS ( + SELECT 1 FROM public.agents WHERE user_id IS NULL AND name = 'Builder' +); + +INSERT INTO public.agents (name, role, api_provider, model, system_prompt) +SELECT + 'Reviewer', + 'Quality Reviewer', + 'openai', + 'gpt-4o', + 'You review outputs for correctness, security, completeness, and missing tests.' +WHERE NOT EXISTS ( + SELECT 1 FROM public.agents WHERE user_id IS NULL AND name = 'Reviewer' +); diff --git a/database/enterprise_security.sql b/database/enterprise_security.sql new file mode 100644 index 0000000000000000000000000000000000000000..32ffc8bc72bdc2f63419cc22530d6adc629b2dee --- /dev/null +++ b/database/enterprise_security.sql @@ -0,0 +1,53 @@ +-- Enterprise Teams Table +CREATE TABLE IF NOT EXISTS teams ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Team Members Table +CREATE TABLE IF NOT EXISTS team_members ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + team_id UUID REFERENCES teams(id) ON DELETE CASCADE, + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + role TEXT CHECK (role IN ('admin', 'editor', 'viewer')) DEFAULT 'viewer', + UNIQUE(team_id, user_id) +); + +-- Add team_id to Projects +ALTER TABLE projects ADD COLUMN IF NOT EXISTS team_id UUID REFERENCES teams(id); + +-- Advanced RLS for Projects +ALTER TABLE projects ENABLE ROW LEVEL SECURITY; + +-- Policy: Users can view projects of their teams +CREATE POLICY "Team members can view team projects" ON projects + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM team_members + WHERE team_members.team_id = projects.team_id + AND team_members.user_id = auth.uid() + ) + ); + +-- Policy: Only admins and editors can modify projects +CREATE POLICY "Team admins and editors can modify projects" ON projects + FOR ALL USING ( + EXISTS ( + SELECT 1 FROM team_members + WHERE team_members.team_id = projects.team_id + AND team_members.user_id = auth.uid() + AND team_members.role IN ('admin', 'editor') + ) + ); + +-- Audit Logs for RLS +CREATE POLICY "Team members can view audit logs" ON audit_logs + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM projects + JOIN team_members ON team_members.team_id = projects.team_id + WHERE projects.id = audit_logs.task_id -- Simplified link + AND team_members.user_id = auth.uid() + ) + ); diff --git a/database/fix_profiles_recursion.sql b/database/fix_profiles_recursion.sql new file mode 100644 index 0000000000000000000000000000000000000000..2eabb5109cbe9fdb75411633960128d86828ee78 --- /dev/null +++ b/database/fix_profiles_recursion.sql @@ -0,0 +1,33 @@ +-- Fix recursive policies in profiles table +-- This migration replaces the existing admin policies with a non-recursive approach + +-- 1. Drop existing problematic policies +DROP POLICY IF EXISTS "Admins can read all profiles" ON public.profiles; +DROP POLICY IF EXISTS "Admins can update all profiles" ON public.profiles; + +-- 2. Create a helper function to check admin status without triggering RLS recursion +-- SECURITY DEFINER runs with the privileges of the creator (usually postgres/service_role) +-- effectively bypassing RLS on the profiles table for this check. +CREATE OR REPLACE FUNCTION public.is_admin_check() +RETURNS BOOLEAN AS $$ +BEGIN + RETURN EXISTS ( + SELECT 1 FROM public.profiles + WHERE id = auth.uid() AND role = 'admin' + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public; + +-- 3. Re-create policies using the helper function +CREATE POLICY "Admins can read all profiles" ON public.profiles + FOR SELECT + USING ( public.is_admin_check() ); + +CREATE POLICY "Admins can update all profiles" ON public.profiles + FOR UPDATE TO authenticated + USING ( public.is_admin_check() ) + WITH CHECK ( public.is_admin_check() ); + +-- 4. Grant access to the function +GRANT EXECUTE ON FUNCTION public.is_admin_check() TO authenticated; +GRANT EXECUTE ON FUNCTION public.is_admin_check() TO service_role; diff --git a/database/marketplace.sql b/database/marketplace.sql new file mode 100644 index 0000000000000000000000000000000000000000..ce3bf2a31e732556f973015b3e4abfaeb7dc139c --- /dev/null +++ b/database/marketplace.sql @@ -0,0 +1,31 @@ +-- Agent Marketplace Table +CREATE TABLE IF NOT EXISTS agent_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + role TEXT NOT NULL, + description TEXT, + model TEXT NOT NULL, + api_provider TEXT NOT NULL, + system_prompt TEXT, + category TEXT, -- e.g., 'Marketing', 'Development', 'Legal' + author_id UUID REFERENCES auth.users(id), + is_featured BOOLEAN DEFAULT false, + usage_count INT DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- RLS for Marketplace (Public View) +ALTER TABLE agent_templates ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Anyone can view templates" ON agent_templates + FOR SELECT USING (true); + +CREATE POLICY "Users can create their own templates" ON agent_templates + FOR INSERT WITH CHECK (auth.uid() = author_id); + +-- Seed some marketplace templates +INSERT INTO agent_templates (name, role, description, model, api_provider, category, system_prompt) +VALUES +('Growth Hacker', 'Marketing Expert', 'Optimizes funnels and generates viral content ideas.', 'gpt-4o', 'openai', 'Marketing', 'You are a Growth Hacker focused on low-cost, high-impact strategies.'), +('Code Architect', 'Senior Developer', 'Designs robust software architectures and reviews code.', 'gpt-4o', 'openai', 'Development', 'You are a Code Architect. Focus on scalability, security, and clean code.'), +('Legal Analyst', 'Legal Advisor', 'Analyzes contracts and identifies legal risks.', 'gpt-4o', 'openai', 'Legal', 'You are a Legal Analyst. Review documents with high precision and caution.'); diff --git a/database/phase3_updates.sql b/database/phase3_updates.sql new file mode 100644 index 0000000000000000000000000000000000000000..40d2d59a2d964cb33972939c0e882a2d2a8bd52b --- /dev/null +++ b/database/phase3_updates.sql @@ -0,0 +1,30 @@ +-- Audit Logs Table +CREATE TABLE IF NOT EXISTS audit_logs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES auth.users(id), + agent_id UUID REFERENCES agents(id), + task_id UUID REFERENCES tasks(id), + action TEXT NOT NULL, + metadata JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Feedback Table for Fine-tuning +CREATE TABLE IF NOT EXISTS task_feedback ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + task_id UUID REFERENCES tasks(id) UNIQUE, + user_id UUID REFERENCES auth.users(id), + rating INT CHECK (rating IN (-1, 1)), -- -1 for dislike, 1 for like + comment TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Add RLS to new tables +ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY; +ALTER TABLE task_feedback ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Users can view their own audit logs" ON audit_logs + FOR SELECT USING (auth.uid() = user_id); + +CREATE POLICY "Users can manage their own feedback" ON task_feedback + FOR ALL USING (auth.uid() = user_id); diff --git a/database/profiles_admin.sql b/database/profiles_admin.sql new file mode 100644 index 0000000000000000000000000000000000000000..17f8e43133960e4954b372c3a77402f479622ff2 --- /dev/null +++ b/database/profiles_admin.sql @@ -0,0 +1,128 @@ +-- Apply this migration after database/schema.sql + +ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'profiles' + AND policyname = 'Users can read own profile' + ) THEN + CREATE POLICY "Users can read own profile" ON public.profiles + FOR SELECT + USING (auth.uid() = id); + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'profiles' + AND policyname = 'Users can insert own profile' + ) THEN + CREATE POLICY "Users can insert own profile" ON public.profiles + FOR INSERT TO authenticated + WITH CHECK ( + auth.uid() = id + AND COALESCE(role, 'user') = 'user' + ); + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'profiles' + AND policyname = 'Users can update own profile' + ) THEN + CREATE POLICY "Users can update own profile" ON public.profiles + FOR UPDATE TO authenticated + USING (auth.uid() = id) + WITH CHECK ( + auth.uid() = id + AND role = ( + SELECT p.role + FROM public.profiles p + WHERE p.id = auth.uid() + ) + ); + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'profiles' + AND policyname = 'Admins can read all profiles' + ) THEN + CREATE POLICY "Admins can read all profiles" ON public.profiles + FOR SELECT + USING ( + EXISTS ( + SELECT 1 + FROM public.profiles admin_profile + WHERE admin_profile.id = auth.uid() + AND admin_profile.role = 'admin' + ) + ); + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'profiles' + AND policyname = 'Admins can update all profiles' + ) THEN + CREATE POLICY "Admins can update all profiles" ON public.profiles + FOR UPDATE TO authenticated + USING ( + EXISTS ( + SELECT 1 + FROM public.profiles admin_profile + WHERE admin_profile.id = auth.uid() + AND admin_profile.role = 'admin' + ) + ) + WITH CHECK ( + role IN ('user', 'admin') + AND EXISTS ( + SELECT 1 + FROM public.profiles admin_profile + WHERE admin_profile.id = auth.uid() + AND admin_profile.role = 'admin' + ) + ); + END IF; +END $$; + +CREATE OR REPLACE FUNCTION public.handle_new_user_profile() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +BEGIN + INSERT INTO public.profiles (id, role, full_name, avatar_url) + VALUES ( + NEW.id, + 'user', + COALESCE(NEW.raw_user_meta_data ->> 'full_name', NEW.raw_user_meta_data ->> 'name'), + NEW.raw_user_meta_data ->> 'avatar_url' + ) + ON CONFLICT (id) DO NOTHING; + RETURN NEW; +END; +$$; + +DROP TRIGGER IF EXISTS on_auth_user_created_profile ON auth.users; + +CREATE TRIGGER on_auth_user_created_profile +AFTER INSERT ON auth.users +FOR EACH ROW +EXECUTE FUNCTION public.handle_new_user_profile(); + +-- Promote your first administrator manually once: +-- UPDATE public.profiles SET role = 'admin' WHERE id = 'YOUR_USER_UUID'; diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000000000000000000000000000000000000..dec271ebc9127d6090034b436c2e2b80eb061410 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,141 @@ +-- Aubm Database Schema +-- Designed for Supabase (PostgreSQL) + +-- 1. Profiles (User Extensions) +CREATE TABLE IF NOT EXISTS public.profiles ( + id UUID PRIMARY KEY REFERENCES auth.users ON DELETE CASCADE, + role TEXT CHECK (role IN ('user', 'manager', 'admin')) DEFAULT 'user', + full_name TEXT, + avatar_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 2. Projects +CREATE TABLE IF NOT EXISTS public.projects ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + description TEXT, + context TEXT, + owner_id UUID REFERENCES auth.users ON DELETE CASCADE, + status TEXT CHECK (status IN ('active', 'archived', 'completed')) DEFAULT 'active', + is_public BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 3. Agents (AI Identities) +CREATE TABLE IF NOT EXISTS public.agents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES auth.users ON DELETE CASCADE, + name TEXT NOT NULL, + role TEXT, + api_provider TEXT NOT NULL, + model TEXT NOT NULL, + system_prompt TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 4. Tasks (Units of work) +CREATE TABLE IF NOT EXISTS public.tasks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID REFERENCES public.projects ON DELETE CASCADE, + assigned_agent_id UUID REFERENCES public.agents ON DELETE SET NULL, + title TEXT NOT NULL, + description TEXT, + status TEXT CHECK (status IN ('todo', 'in_progress', 'awaiting_approval', 'done', 'failed', 'cancelled')) DEFAULT 'todo', + priority INTEGER DEFAULT 0, + is_critical BOOLEAN DEFAULT FALSE, + output_data JSONB, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 5. Task Runs (Execution History) +CREATE TABLE IF NOT EXISTS public.task_runs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + task_id UUID REFERENCES public.tasks ON DELETE CASCADE, + agent_id UUID REFERENCES public.agents ON DELETE SET NULL, + status TEXT CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')) DEFAULT 'queued', + error_message TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + finished_at TIMESTAMPTZ +); + +-- 6. Agent Logs (Execution Traces) +CREATE TABLE IF NOT EXISTS public.agent_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + task_id UUID REFERENCES public.tasks ON DELETE CASCADE, + run_id UUID REFERENCES public.task_runs ON DELETE CASCADE, + action TEXT, + content TEXT, + metadata JSONB, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 7. App Config (Global Settings) +CREATE TABLE IF NOT EXISTS public.app_config ( + key TEXT PRIMARY KEY, + value JSONB, + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- RLS (Row Level Security) - Initial setup +ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.agents ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.task_runs ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.agent_logs ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.app_config ENABLE ROW LEVEL SECURITY; + +-- Basic Policies (To be refined) +-- Projects: Owners can do anything, others can read if public +CREATE POLICY "Projects visibility" ON public.projects + FOR SELECT USING (auth.uid() = owner_id OR is_public = true); + +CREATE POLICY "Projects ownership" ON public.projects + FOR ALL USING (auth.uid() = owner_id); + +-- Tasks: Protected by project ownership +CREATE POLICY "Tasks visibility" ON public.tasks + FOR SELECT USING (EXISTS ( + SELECT 1 FROM public.projects + WHERE projects.id = tasks.project_id AND (projects.owner_id = auth.uid() OR projects.is_public = true) + )); + +CREATE POLICY "Project owners can create tasks" ON public.tasks + FOR INSERT TO authenticated WITH CHECK (EXISTS ( + SELECT 1 FROM public.projects + WHERE projects.id = tasks.project_id AND projects.owner_id = auth.uid() + )); + +CREATE POLICY "Project owners can update tasks" ON public.tasks + FOR UPDATE TO authenticated USING (EXISTS ( + SELECT 1 FROM public.projects + WHERE projects.id = tasks.project_id AND projects.owner_id = auth.uid() + )) WITH CHECK (EXISTS ( + SELECT 1 FROM public.projects + WHERE projects.id = tasks.project_id AND projects.owner_id = auth.uid() + )); + +CREATE POLICY "Project owners can delete tasks" ON public.tasks + FOR DELETE TO authenticated USING (EXISTS ( + SELECT 1 FROM public.projects + WHERE projects.id = tasks.project_id AND projects.owner_id = auth.uid() + )); + +-- Agents: Marketplace templates are readable by all authenticated users. +-- Deployed agents are owned by the user who deployed them. +CREATE POLICY "Agents readable" ON public.agents + FOR SELECT TO authenticated USING (true); + +CREATE POLICY "Users can create own agents" ON public.agents + FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can update own agents" ON public.agents + FOR UPDATE TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can delete own agents" ON public.agents + FOR DELETE TO authenticated USING (auth.uid() = user_id); diff --git a/database/seed.sql b/database/seed.sql new file mode 100644 index 0000000000000000000000000000000000000000..095217f031d25095337654d9d17909fdea7ab3b8 --- /dev/null +++ b/database/seed.sql @@ -0,0 +1,15 @@ +-- Seed Data for Aubm + +-- 1. Default Agents +INSERT INTO public.agents (name, role, api_provider, model, system_prompt) +VALUES +('GPT-4o', 'General Intelligence', 'openai', 'gpt-4o', 'You are a highly capable AI assistant.'), +('AMD-4o', 'Performance Specialist', 'amd', 'gpt-4o', 'You are a high-performance agent running on AMD infrastructure.'), +('Llama-3-70B', 'Fast Logic', 'groq', 'llama3-70b-8192', 'You are a fast and efficient reasoning agent.'); + +-- 2. Default App Config +INSERT INTO public.app_config (key, value) +VALUES +('output_language', '"en"'), +('max_parallel_tasks', '5'), +('enable_human_loop', 'true'); diff --git a/database/task_dependencies.sql b/database/task_dependencies.sql new file mode 100644 index 0000000000000000000000000000000000000000..2aec3e17ee0a94b18cf30d1cea43f79cb384bc14 --- /dev/null +++ b/database/task_dependencies.sql @@ -0,0 +1,104 @@ +-- Apply this migration after database/schema.sql + +CREATE TABLE IF NOT EXISTS public.task_dependencies ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID NOT NULL REFERENCES public.projects ON DELETE CASCADE, + task_id UUID NOT NULL REFERENCES public.tasks ON DELETE CASCADE, + depends_on_task_id UUID NOT NULL REFERENCES public.tasks ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT task_dependencies_unique UNIQUE (project_id, task_id, depends_on_task_id), + CONSTRAINT task_dependencies_not_self CHECK (task_id <> depends_on_task_id) +); + +CREATE INDEX IF NOT EXISTS idx_task_dependencies_project_id ON public.task_dependencies(project_id); +CREATE INDEX IF NOT EXISTS idx_task_dependencies_task_id ON public.task_dependencies(task_id); +CREATE INDEX IF NOT EXISTS idx_task_dependencies_depends_on_task_id ON public.task_dependencies(depends_on_task_id); + +ALTER TABLE public.task_dependencies ENABLE ROW LEVEL SECURITY; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'task_dependencies' + AND policyname = 'Task dependencies visibility' + ) THEN + CREATE POLICY "Task dependencies visibility" ON public.task_dependencies + FOR SELECT + USING ( + EXISTS ( + SELECT 1 + FROM public.projects + WHERE projects.id = task_dependencies.project_id + AND (projects.owner_id = auth.uid() OR projects.is_public = true) + ) + ); + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'task_dependencies' + AND policyname = 'Project owners can create task dependencies' + ) THEN + CREATE POLICY "Project owners can create task dependencies" ON public.task_dependencies + FOR INSERT TO authenticated + WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.projects + WHERE projects.id = task_dependencies.project_id + AND projects.owner_id = auth.uid() + ) + ); + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'task_dependencies' + AND policyname = 'Project owners can update task dependencies' + ) THEN + CREATE POLICY "Project owners can update task dependencies" ON public.task_dependencies + FOR UPDATE TO authenticated + USING ( + EXISTS ( + SELECT 1 + FROM public.projects + WHERE projects.id = task_dependencies.project_id + AND projects.owner_id = auth.uid() + ) + ) + WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.projects + WHERE projects.id = task_dependencies.project_id + AND projects.owner_id = auth.uid() + ) + ); + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'task_dependencies' + AND policyname = 'Project owners can delete task dependencies' + ) THEN + CREATE POLICY "Project owners can delete task dependencies" ON public.task_dependencies + FOR DELETE TO authenticated + USING ( + EXISTS ( + SELECT 1 + FROM public.projects + WHERE projects.id = task_dependencies.project_id + AND projects.owner_id = auth.uid() + ) + ); + END IF; +END $$; diff --git a/database/task_owner_policies.sql b/database/task_owner_policies.sql new file mode 100644 index 0000000000000000000000000000000000000000..52bf9c5ad60f169ff5913aceddffc79cb1920391 --- /dev/null +++ b/database/task_owner_policies.sql @@ -0,0 +1,63 @@ +-- Task ownership policies for project owners +-- Apply this migration to existing Supabase projects after schema.sql. + +ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'tasks' + AND policyname = 'Project owners can create tasks' + ) THEN + CREATE POLICY "Project owners can create tasks" ON public.tasks + FOR INSERT TO authenticated WITH CHECK ( + EXISTS ( + SELECT 1 FROM public.projects + WHERE projects.id = tasks.project_id + AND projects.owner_id = auth.uid() + ) + ); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'tasks' + AND policyname = 'Project owners can update tasks' + ) THEN + CREATE POLICY "Project owners can update tasks" ON public.tasks + FOR UPDATE TO authenticated USING ( + EXISTS ( + SELECT 1 FROM public.projects + WHERE projects.id = tasks.project_id + AND projects.owner_id = auth.uid() + ) + ) WITH CHECK ( + EXISTS ( + SELECT 1 FROM public.projects + WHERE projects.id = tasks.project_id + AND projects.owner_id = auth.uid() + ) + ); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'tasks' + AND policyname = 'Project owners can delete tasks' + ) THEN + CREATE POLICY "Project owners can delete tasks" ON public.tasks + FOR DELETE TO authenticated USING ( + EXISTS ( + SELECT 1 FROM public.projects + WHERE projects.id = tasks.project_id + AND projects.owner_id = auth.uid() + ) + ); + END IF; +END $$; + +NOTIFY pgrst, 'reload schema'; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..f12f9601e9b41a75f5e9f62b87eed7dd163bc24e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "8000:8000" + env_file: + - ./backend/.env + environment: + - ALLOWED_ORIGINS=http://localhost:80,http://localhost:5173 + volumes: + - ./backend/outputs:/app/outputs + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "80:80" + environment: + - VITE_API_URL=http://localhost:8000 + depends_on: + - backend + restart: unless-stopped diff --git a/docs/AUDIT.md b/docs/AUDIT.md new file mode 100644 index 0000000000000000000000000000000000000000..7f733ce314d0c1f6faef66a15155c58b06de422b --- /dev/null +++ b/docs/AUDIT.md @@ -0,0 +1,45 @@ +# 🛡️ Aubm System Stability Audit Report + +**Date**: May 4, 2026 +**Status**: Stable / Production-Ready (Phase 4 Initialized) + +## 🏗️ Architecture Overview +The system follows a modular micro-service pattern using **FastAPI** (Python) and **React 18** (Vite). +- **Backend**: Highly decoupled agent-provider pattern with a centralized `ToolRegistry`. +- **Frontend**: Glassmorphic UI with real-time SSE logging and mobile readiness (Capacitor). + +## 🔒 Security & Governance +- [x] **Authentication**: Supabase Auth with SSO (Google/GitHub) support. +- [x] **Authorization**: Advanced RLS (Row Level Security) with team-based isolation and role-based access control (Admin/Editor/Viewer). +- [x] **Auditing**: Every agent action and LLM call is recorded in `audit_logs` for compliance. + +## 🤖 Agent Capabilities +| Tool | Stability | Notes | +| :--- | :--- | :--- | +| **BrowserTool** | High | Integrated with Playwright for reliable web research. | +| **CodeSandbox** | High | Isolated Python execution for logical verification. | +| **FileGenerator** | High | Professional PDF/Excel generation (ReportLab/Pandas). | +| **Decomposer** | High | Enables recursive agent autonomy (project planning). | +| **SRE Tool** | High | System health monitoring and whitelisted autonomous patching. | + +## 📊 Database Health +- Schema is partitioned across 4 main upgrade files (`schema.sql`, `phase3_updates.sql`, `marketplace.sql`, `enterprise_security.sql`). +- All tables include proper foreign key constraints and RLS policies. +- **Seeding**: Initial agent experts and project templates are pre-loaded. + +## 🚀 Autonomous Reliability (Phase 4) +- **Self-Healing**: The SRE agent can now detect service failures and apply whitelisted patches (e.g., `git pull`, `npm install`). +- **Safety**: Whitelist prevents destructive commands, ensuring the agent cannot harm the host OS. +- **Next-Gen Interfaces**: Voice control and the spatial DAG viewer are scaffolded in the frontend for hands-free status checks and immersive task-flow inspection. + +## Operations Readiness (Phase 5) +- **Monitoring Endpoint**: `GET /monitoring/summary` reports API/database health and core workflow counts. +- **Operations Dashboard**: The frontend includes a monitoring view with backend-first status checks and Supabase fallback metrics. + +## 💡 Recommendations for Next Steps +1. **Production Deployment**: Finalize Dockerization for isolated backend deployment. +2. **Monitoring**: Integrate Sentry or Datadog for real-time error tracking. +3. **Intelligence**: Begin fine-tuning local models (Ollama) using the captured `task_feedback` data. + +--- +**Verdict**: The system is robust, secure, and ready for autonomous project orchestration at scale. diff --git a/docs/OPERATING_GUIDE.md b/docs/OPERATING_GUIDE.md new file mode 100644 index 0000000000000000000000000000000000000000..f00ca1a40ef9646c5f77458c60f9d60654da2b19 --- /dev/null +++ b/docs/OPERATING_GUIDE.md @@ -0,0 +1,280 @@ +# Aubm Operating Guide + +## What Aubm Does + +Aubm is an AI agent orchestration platform. Users sign in with Supabase Auth, deploy or configure agents, assign them to tasks, run autonomous executions, review outputs, and monitor system health from a React dashboard. + +The application has three main layers: + +- `frontend/`: React + Vite dashboard for authentication, marketplace, debates, voice control, spatial task visualization, and monitoring. +- `backend/`: FastAPI API for task execution, multi-agent debate orchestration, tool calling, and monitoring. +- `database/`: Supabase SQL schema, seed data, RLS policies, marketplace tables, audit logs, teams, and migrations. + +## Core Runtime Flow + +1. A user signs in through Supabase Auth. +2. The frontend reads templates, agents, projects, and tasks from Supabase. +3. A user deploys an agent from the marketplace into `public.agents`. +4. A task references an assigned agent through `tasks.assigned_agent_id`. +5. `POST /tasks/{task_id}/run` starts backend execution. +6. The backend loads the task, assigned agent, and previous completed task outputs. +7. `AgentFactory` creates the right provider implementation, currently `OpenAIAgent` or `AMDAgent`. +8. The agent produces JSON output. +9. The backend writes output to `tasks.output_data`, moves the task to `awaiting_approval`, records `task_runs`, `agent_logs`, and `audit_logs`. +10. A human reviews, edits, approves, or gives feedback through the frontend. + +## Main Features + +### Dashboard + +Shows project cards and high-level workflow progress. It is currently a static dashboard scaffold, ready to be connected to live project data. + +### Agent Marketplace + +Reads `agent_templates` from Supabase and deploys selected templates into `agents`. + +Required database support: + +- `agents.user_id` +- Insert policy allowing authenticated users to create agents where `auth.uid() = user_id` + +Apply: + +```sql +-- database/agent_ownership.sql +``` + +### Custom Agents + +The `Agents` screen lets users create custom agents directly. + +Each agent has: + +- Name +- Role +- LLM provider +- Model +- System prompt + +The currently wired backend providers are: + +- `openai` +- `amd` + +Settings stores the frontend default provider/model in browser local storage. Provider API keys are never stored in the frontend; they must stay in `backend/.env`. + +### Agent Debate + +Uses the backend endpoint: + +```text +POST /orchestrator/debate +``` + +The flow is: + +1. Agent A generates an initial answer. +2. Agent B critiques the answer. +3. Agent A refines the output. +4. The final debate result is saved to `tasks.output_data`. + +### Voice Control + +Uses browser Web Speech APIs. + +Supported commands include: + +- `dashboard` +- `marketplace` +- `debate` +- `settings` +- `new project` +- `status` + +The `status` command reads project/task counts from Supabase and speaks the result. + +### Spatial View + +Shows a layered task DAG-style visualization. It reads recent tasks from Supabase and falls back to demo nodes if no tasks are available. + +### Monitoring + +Uses: + +```text +GET /monitoring/summary +``` + +The endpoint reports: + +- API status +- Database status +- Project count +- Task count +- Agent count +- Task run count +- Failed tasks +- Tasks awaiting approval + +If the backend endpoint is unavailable, the frontend falls back to direct Supabase count queries. + +## Backend Setup + +From `backend/`: + +```powershell +python -m venv venv +.\venv\Scripts\activate +pip install -r requirements.txt +uvicorn main:app --reload --port 8000 +``` + +Required `backend/.env` values: + +```env +SUPABASE_URL=... +SUPABASE_SERVICE_ROLE_KEY=... +OPENAI_API_KEY=... +AMD_API_KEY=... +``` + +Optional provider keys: + +```env +GROQ_API_KEY=... +GEMINI_API_KEY=... +ANTHROPIC_API_KEY=... +``` + +## Frontend Setup + +From `frontend/`: + +```powershell +npm install +npm run dev +``` + +Required `frontend/.env` values: + +```env +VITE_SUPABASE_URL=... +VITE_SUPABASE_ANON_KEY=... +VITE_API_URL=http://127.0.0.1:8000 +``` + +Build check: + +```powershell +npm run build +``` + +## Database Setup Order + +Apply SQL files in this order for a fresh Supabase project: + +1. `database/schema.sql` +2. `database/seed.sql` +3. `database/phase3_updates.sql` +4. `database/marketplace.sql` +5. `database/enterprise_security.sql` +6. `database/agent_ownership.sql` +7. `database/task_owner_policies.sql` +8. `database/default_agents.sql` + +For an existing Supabase project where marketplace deploy fails with missing `user_id`, apply only: + +```sql +-- database/agent_ownership.sql +``` + +Then reload the frontend with a hard refresh. + +## Important Tables + +- `profiles`: User metadata and role. +- `projects`: Project containers. +- `agents`: Deployed AI agents. +- `agent_templates`: Marketplace templates. +- `tasks`: Work units assigned to agents. +- `task_runs`: Execution history. +- `agent_logs`: Agent execution traces. +- `audit_logs`: Governance and compliance trail. +- `task_feedback`: Like/dislike feedback for future tuning. +- `teams` and `team_members`: Enterprise team permissions. + +## Tool System + +The backend exposes tools to agents through `tools/registry.py`. + +Available tools include: + +- Web extraction with Playwright. +- Python code execution. +- PDF generation. +- Excel generation. +- Project decomposition. +- System health checks. +- Restricted patch commands. + +## Current Roadmap State + +Completed: + +- Core backend and frontend foundation. +- Supabase auth and schema. +- Agent execution. +- Multi-agent debate. +- Marketplace. +- Voice control. +- Spatial task viewer. +- Operations monitoring. + +In progress: + +- Production operations hardening. +- Error tracking. +- Docker/runtime packaging. +- Frontend bundle splitting. +- Production CORS allowlist. + +## Common Errors + +### `403 Forbidden` on `POST /rest/v1/agents` + +Cause: RLS policy does not allow insert. + +Fix: Apply `database/agent_ownership.sql`. + +### `Could not find the 'user_id' column of 'agents' in the schema cache` + +Cause: `agents.user_id` is missing or PostgREST schema cache has not reloaded. + +Fix: + +```sql +ALTER TABLE public.agents +ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users ON DELETE CASCADE; + +NOTIFY pgrst, 'reload schema'; +``` + +Then hard refresh the frontend. + +### `OTS parsing error` + +Cause: A CSS URL was incorrectly used as a font file. + +Fix: Use Google Fonts through `@import`, already applied in `frontend/src/styles/variables.css`. + +### Frontend chunk-size warning + +Vite currently warns that the JS chunk is larger than 500 KB. This is not a runtime error. The Phase 5 roadmap includes bundle splitting. + +## Development Rules + +- Keep frontend display text in English. +- Keep documentation in English. +- Keep database migrations idempotent when possible. +- Never commit real secrets from `.env`. +- Prefer applying database changes through separate migration files instead of editing only `schema.sql`. diff --git a/docs/SALES_ONE_PAGER.md b/docs/SALES_ONE_PAGER.md new file mode 100644 index 0000000000000000000000000000000000000000..e203464627898fbac770ea7ca8163512269194d7 --- /dev/null +++ b/docs/SALES_ONE_PAGER.md @@ -0,0 +1,148 @@ +# Aubm + +## Descripcion + +Aubm es una plataforma de orquestacion de agentes de IA disenada para convertir objetivos complejos en trabajo ejecutable, supervisable y trazable. + +Permite crear proyectos, definir contexto, cargar fuentes, generar agentes especializados, descomponer objetivos en tareas, asignarlas, establecer dependencias y ejecutar flujos de trabajo con revision humana en el circuito. En lugar de usar un solo chat aislado, Aubm organiza el trabajo como un sistema operativo para equipos y agentes: planifica, coordina, monitorea y consolida resultados. + +## Propuesta de valor + +- Orquestacion multiagente para investigacion, analisis, planificacion y ejecucion +- Human-in-the-loop para aprobar, corregir o relanzar resultados antes de cerrar trabajo +- Soporte multi-LLM para operar con distintos proveedores y modelos +- Gestion estructurada de proyectos con tareas, dependencias, asignacion por agente y reportes +- Contexto enriquecido con links, notas, documentos y busqueda web +- Dos experiencias de uso: + - Guided para equipos que quieren velocidad y simplicidad + - Expert para usuarios que necesitan control fino + +## Problema que resuelve + +La mayoria de los equipos usa IA como conversaciones sueltas, sin estructura operativa, sin trazabilidad y sin mecanismos claros de supervision. Eso genera: + +- informacion dispersa +- baja reutilizacion del contexto +- dificultad para coordinar multiples agentes o tareas +- poca gobernanza sobre resultados +- debilidad para convertir analisis en ejecucion real + +Aubm resuelve eso llevando la IA desde el chat aislado a un flujo de trabajo estructurado y auditable. + +## Potenciales clientes + +### 1. Equipos de estrategia y research + +Ideal para equipos que necesitan investigar mercados, competidores, tendencias, categorias y oportunidades con rapidez, pero sin perder control ni trazabilidad. + +Ejemplos: + +- consultoras +- equipos de estrategia corporativa +- research interno de producto +- intelligence teams + +### 2. Equipos de operaciones y automatizacion interna + +Util para organizaciones que quieren coordinar procesos internos con IA, documentar decisiones, asignar trabajo y supervisar ejecucion. + +Ejemplos: + +- operaciones +- PMO +- equipos de mejora de procesos +- transformation offices + +### 3. Product teams y builders de IA + +Sirve para equipos que construyen experiencias con LLMs y necesitan orquestar agentes, herramientas, tareas y revisiones dentro de una misma interfaz. + +Ejemplos: + +- startups AI-native +- equipos de producto +- equipos de platform engineering +- labs de innovacion + +### 4. Equipos de analisis competitivo + +Especialmente util para areas que monitorean el mercado y necesitan producir comparativas, matrices, gaps de producto y recomendaciones accionables. + +Ejemplos: + +- product marketing +- strategy +- business development +- founders y leadership teams + +### 5. Organizaciones que generan reportes ejecutivos + +Aubm ayuda a transformar inputs dispersos en entregables consistentes, revisables y listos para decision. + +Ejemplos: + +- equipos de direccion +- chief of staff +- operaciones comerciales +- areas de reporting ejecutivo + +### 6. Empresas que quieren coordinacion humano + IA + +Buen fit para companias que no quieren automatizacion ciega, sino un modelo donde la IA trabaja con supervision humana, aprobaciones y control de calidad. + +Ejemplos: + +- empresas reguladas +- fintech +- healthtech +- legaltech +- enterprise SaaS con procesos sensibles + +## Buyer personas + +### Founder / CEO + +Busca velocidad de ejecucion, claridad operativa y capacidad de convertir analisis en decisiones concretas. + +### Head of Strategy + +Necesita investigacion estructurada, comparativas reproducibles y reportes confiables para direccion. + +### COO / Operations Lead + +Quiere procesos mas ordenados, seguimiento de tareas, dependencias claras y visibilidad sobre ejecucion. + +### Product Manager / Head of Product + +Busca coordinar exploracion, definicion, research, roadmap y analisis competitivo usando IA de forma estructurada. + +### Innovation Lead / AI Lead + +Necesita una capa operativa para agentes de IA que permita experimentar, monitorear y gobernar flujos multiagente. + +## Casos de uso comerciales + +- analisis competitivo +- research de mercado +- generacion de roadmap +- descomposicion de proyectos complejos +- coordinacion de agentes especialistas +- produccion de reportes ejecutivos +- automatizacion supervisada de flujos internos +- investigacion con fuentes y busqueda web + +## Diferenciales + +- no es solo chat: es ejecucion estructurada +- no es solo automatizacion: incorpora supervision humana +- no depende de un solo modelo: opera con multiples proveedores +- no se limita al analisis: tambien organiza tareas, dependencias y resultados +- puede servir tanto a usuarios nuevos como avanzados con sus modos Guided y Expert + +## Mensaje corto para venta + +Aubm convierte la IA de conversaciones sueltas en ejecucion real: proyectos, agentes, tareas, supervision y resultados en una sola plataforma. + +## Pitch corto + +Aubm es la capa operativa para equipos que quieren trabajar con agentes de IA sin perder control. Organiza objetivos, contexto, tareas, agentes, dependencias y aprobaciones dentro de un flujo supervisable y trazable, listo para investigacion, operaciones, analisis competitivo y reportes ejecutivos. diff --git a/docs/TASKS.md b/docs/TASKS.md new file mode 100644 index 0000000000000000000000000000000000000000..5513176c7c675fab6e9920bd02cd23d9a6bf37f8 --- /dev/null +++ b/docs/TASKS.md @@ -0,0 +1,101 @@ +# Project Tasks: Aubm Implementation + +This file tracks the granular implementation steps for the Aubm platform, following the [ROADMAP.md](./ROADMAP.md) and [SPEC.md](./SPEC.md). + +## Phase 1: Core Foundation + +### 1.1 Project Initialization +- [x] Create directory structure (`backend/`, `frontend/`, `database/`) +- [x] Initialize Python virtual environment in `backend/` +- [x] Initialize Vite + React + TS project in `frontend/` +- [x] Create `backend/requirements.txt` with core dependencies +- [x] Create `frontend/package.json` and install dependencies + +### 1.2 Database & Schema +- [x] Create `database/schema.sql` based on SPEC.md +- [x] Set up Supabase project +- [x] Implement initial seed data for `agents` and `app_config` + +### 1.3 Backend Core +- [x] Implement `backend/main.py` entrypoint +- [x] Create `backend/services/config.py` for environment management +- [x] Implement `backend/agents/base.py` +- [x] Implement first agent providers (`OpenAIAgent`, `AMDAgent`) +- [x] Implement `backend/routers/agent_runner.py` for task execution + +### 1.4 Frontend Core +- [x] Set up CSS design system +- [x] Implement Supabase Auth integration +- [x] Create app layout with sidebar and header +- [x] Build project dashboard view + +## Phase 2: Advanced Collaboration & Tools + +### 2.1 Extended Toolbelt +- [x] Implement `BrowserTool` using Playwright +- [x] Create `ToolRegistry` for agent access +- [x] Implement `CodeSandboxTool` +- [x] Add file generation capabilities + +### 2.2 Multi-Agent Features +- [x] Implement debate logic +- [x] Create peer review status/dashboard for tasks + +### 2.3 Real-Time Collaboration +- [x] Implement collaborative output editor +- [ ] Real-time cursor/presence indicators + +### 2.4 Mobile Experience +- [x] Initialize Capacitor in the frontend project +- [ ] Add Android/iOS platform scaffolding + +## Phase 3: Intelligence & Scale + +### 3.1 Advanced Analytics & Security +- [x] Implement audit logs for LLM interaction tracking +- [x] Add feedback loop for fine-tuning data collection +- [x] Implement SSO integration through Supabase +- [x] Implement granular RLS for project teams + +### 3.2 Recursive Autonomy +- [x] Implement project decomposition +- [x] Create agent marketplace schema and gallery UI + +## Phase 4: Autonomy & Beyond + +### 4.1 System Self-Healing +- [x] Implement health check agents +- [x] Create restricted autonomous patching logic + +### 4.2 Next-Gen Interfaces +- [x] Voice control integration +- [x] Scaffolding for VR/AR project viewer + +## Phase 5: Production Operations + +### 5.1 Observability +- [x] Add backend monitoring summary endpoint +- [x] Add frontend operations monitoring dashboard +- [x] Add external error tracking integration + +### 5.2 Deployment Hardening +- [x] Add Dockerfile and production server command +- [x] Replace wildcard CORS with environment-driven allowlist +- [x] Add frontend bundle splitting/performance budget + +## Phase 6: Distributed Scale & Intelligence + +### 6.1 Asynchronous Workers +- [/] Implement `backend/worker.py` for task consumption +- [/] Implement `backend/services/task_queue.py` using a lightweight polling or webhook mechanism + +### 6.2 Advanced Memory +- [ ] Set up pgvector extension in Supabase +- [ ] Implement semantic retrieval service for cross-project context + +### 6.3 Self-Optimization +- [ ] Create agent "reflection" router to analyze task history +- [ ] Implement automated system prompt refinement logic + +--- +*Legend: Pending | In Progress | Completed* diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..a2a665d7a0e351e4f1d77a6a0bbdd1a5f7e2d9ec --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,6 @@ +VITE_API_URL=http://localhost:8000 +VITE_SUPABASE_URL=https://your-project-id.supabase.co +VITE_SUPABASE_ANON_KEY=your-anon-key-here + +# Optional: Sentry +VITE_SENTRY_DSN=your-sentry-dsn diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..2e51b702a17e1c2466e3c11fe0e18c57695d7598 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,44 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +# Python virtual env +venv/ +backend/venv/ +**/venv/ + +# Python cache +__pycache__/ +*.pyc +*.pyd + +# Env files +.env +.env.* +**/.env +**/.env.* + +# Permitir ejemplos sin secretos +!.env.example +!**/.env.example + diff --git a/frontend/.vite/deps/_metadata.json b/frontend/.vite/deps/_metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..6ca79682d42468d40b0f6be81d6e4facebbe6d36 --- /dev/null +++ b/frontend/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "06cd0f54", + "configHash": "6f67514f", + "lockfileHash": "7cb592dd", + "browserHash": "15e48278", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/frontend/.vite/deps/package.json b/frontend/.vite/deps/package.json new file mode 100644 index 0000000000000000000000000000000000000000..3dbc1ca591c0557e35b6004aeba250e6a70b56e3 --- /dev/null +++ b/frontend/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..55c152a3050e1f265a98a5c518b75139a528c7ea --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,20 @@ +# Build stage +FROM node:20-slim AS build-stage + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . +RUN npm run build + +# Production stage +FROM nginx:stable-alpine AS production-stage + +COPY --from=build-stage /app/dist /usr/share/nginx/html +COPY --from=build-stage /app/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7dbf7ebf3b2a3d84ad526bc47810d1d211331b8b --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/capacitor.config.ts b/frontend/capacitor.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa18ce75cc3cdf9f255b88cc02f890229ed556dd --- /dev/null +++ b/frontend/capacitor.config.ts @@ -0,0 +1,9 @@ +import type { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'com.aubm.app', + appName: 'Aubm', + webDir: 'dist' +}; + +export default config; diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..c2a386b181c6464d931bb24f298b9fd085ae3000 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,25 @@ +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 { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + rules: { + 'react-hooks/set-state-in-effect': 'off', + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000000000000000000000000000000000..42bcd27ffa5e0759245a27769da8b70429385fb6 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + frontend + + +
+ + + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..80fcbc792ecb28fbfadd9e225dcc9dd01f2afc4b --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to the backend if needed + # location /api/ { + # proxy_pass http://backend:8000/; + # } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..7cb144a339fff7e43f27837445bd9289a7cad059 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4442 @@ +{ + "name": "frontend", + "version": "0.7.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.7.0", + "dependencies": { + "@capacitor/android": "^8.3.1", + "@capacitor/cli": "^7.6.2", + "@capacitor/core": "^8.3.1", + "@capacitor/ios": "^8.3.1", + "@sentry/react": "^8.54.0", + "@supabase/supabase-js": "^2.105.1", + "framer-motion": "^12.38.0", + "i18next": "^26.0.8", + "lucide-react": "^1.14.0", + "react-i18next": "^17.0.6", + "react-router-dom": "^7.1.5", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.2", + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.7.0", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^6.4.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capacitor/android": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.3.1.tgz", + "integrity": "sha512-hjskIG8YcBEh3X4yaTXvE9gcqpdcxunTgFruSKnuPxtMxAUzEK4Oq25x0Z1g3cz+MQPc+lRG09R7Ovc+ydKsNw==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.3.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.6.2.tgz", + "integrity": "sha512-uPm+GDVhdWrM/DBWZ/L6c8uBVaEcge4MAXhqrIJWSkwad/9vNoVfUjtHaVgXxPE1g399PhlGm4kU8U7Qdfmwow==", + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@capacitor/core": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.1.tgz", + "integrity": "sha512-UF8ItlHguU1Z6GXfPTeT2gakf+ctNI8pAS1kwSBQlsJMlfD4OPoto/SmKnOxKCQvnF4WRcdWeg6C0zREUNaAQg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@capacitor/ios": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.3.1.tgz", + "integrity": "sha512-BEhLyYYHWJLib4mpaPMaaylbC8meqgxbNYwQJH2svsSLW7yo/hFie+Zoo66a44XnqcMd2tvmAuzimWunXZi/xA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "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/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.55.2.tgz", + "integrity": "sha512-GnKod+gL/Y+1FUM/RGV8q6le1CoyiGbT40MitEK7eVwWe+bfTRq1gN7ioupyHFMUg1RlQkDQ4/sENmio/uow5A==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.55.2.tgz", + "integrity": "sha512-XQy//NWbL0mLLM5w8wNDWMNpXz39VUyW2397dUrH8++kR63WhUVAvTOtL0o0GMVadSAzl1b08oHP9zSUNFQwcg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.55.2.tgz", + "integrity": "sha512-+W43Z697EVe/OgpGW07B773sa8xO1UbpnW0Cr+E+3FMDb6ZbXlaBUoagPTUkkQPdwBe35SDh6r8y2M3EOPGbxg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.2", + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.55.2.tgz", + "integrity": "sha512-P/jGiuR7dRLG9IzD/463fLgiibyYceauav/9prRG0ZxJm1AtuO02OKball2Fs3bbzdzwHCTlcsUuL2ivDF4b5A==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "8.55.2", + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/browser": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.55.2.tgz", + "integrity": "sha512-xHuPIEKhx9zw5quWvv4YgZprnwoVMCfxIhmOIf6KJ9iizyUHeUDcKpLS59xERroqwX4RpvK+l/27AZu4zfZlzQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.2", + "@sentry-internal/feedback": "8.55.2", + "@sentry-internal/replay": "8.55.2", + "@sentry-internal/replay-canvas": "8.55.2", + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/core": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.2.tgz", + "integrity": "sha512-YlEBwybUcOQ/KjMHDmof1vwweVnBtBxYlQp7DE3fOdtW4pqqdHWTnTntQs4VgYfxzjJYgtkd9LHlGtg8qy+JVQ==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/react": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.55.2.tgz", + "integrity": "sha512-1TPfKZYkJal2Dyt2W0tf1roOZmu7sqr6/dTqjdsuu2WgGTilMEreK26YqB8ROOYdMjkVJpNCcIKXQHyMp2eCwA==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "8.55.2", + "@sentry/core": "8.55.2", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.105.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.105.1.tgz", + "integrity": "sha512-zc4s8Xg4truwE1Q4Q8M8oUVDARMd05pKh73NyQsMbYU1HDdDN2iiKzena/yu+yJze3WrD4c092FdckPiK1rLQw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.105.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.105.1.tgz", + "integrity": "sha512-dTk1e7oE51VGc1lS2S0J0NLo0Wp4JYChj74ArJKbIWgoWuFwO0wcJYjeyOV3AAEpKst8/LQWUZOUKO1tRXBrpA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.1.tgz", + "integrity": "sha512-hWGJkDAfWUNY8k0C080u3sGNFd2ncl9erhKgP7hnGkgJWEfT5Pd/SXal4QmWXBECVlZrannMAc9sBaaRyWpiUA==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.105.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.105.1.tgz", + "integrity": "sha512-6SbtsoWC55xfsm7gbfLqvF+yIwTQEbjt+jFGf4klDpwSnUy17Hv5x0Dq52oqwTQlw6Ta0h1D5gTP0/pApqNojA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.105.1", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.105.1.tgz", + "integrity": "sha512-3X3cUEl5cJ4lRQHr1hXHx0b98OaL97RRO2vrRZ98FD91JV/MquZHhrGJSv/+IkOnjF6E2e0RUOxE8P3Zi035ow==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.1", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.105.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.105.1.tgz", + "integrity": "sha512-owfdCNH5ikXXDusjzsgU6LavEBqGUoueOnL/9XIucld70/WJ/rbqp89K//c9QPICDNuegsmpoeasydDAiucLKQ==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.105.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.105.1.tgz", + "integrity": "sha512-4gn6HmsAkCCVU7p8JmgKGhHJ5Btod4ZzSp8qKZf4JHaTxbhaIK86/usHzeLxWv7EJJDhBmILDmJOSOf9iF4CLA==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.105.1", + "@supabase/functions-js": "2.105.1", + "@supabase/postgrest-js": "2.105.1", + "@supabase/realtime-js": "2.105.1", + "@supabase/storage-js": "2.105.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", + "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/type-utils": "8.59.1", + "@typescript-eslint/utils": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.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.59.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.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.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz", + "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", + "debug": "^4.4.3" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", + "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.1", + "@typescript-eslint/types": "^8.59.1", + "debug": "^4.4.3" + }, + "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 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", + "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1" + }, + "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.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", + "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", + "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 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz", + "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/utils": "8.59.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", + "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", + "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.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", + "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.1", + "@typescript-eslint/tsconfig-utils": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.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 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", + "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", + "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "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.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", + "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "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/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/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==", + "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==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.349", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", + "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", + "dev": true, + "license": "ISC" + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", + "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.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", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "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/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/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-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/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/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/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.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "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/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.0.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.8.tgz", + "integrity": "sha512-BRzLom0mhDhV9v0QhgUUHWQJuwFmnr1194xEcNLYD6ym8y8s542n4jXUvRLnhNTbh9PmpU6kGZamyuGHQMsGjw==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "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/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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-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/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/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/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "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.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/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/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-i18next": { + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.6.tgz", + "integrity": "sha512-WzJ6SMKF+GTD7JZZqxSR1AKKmXjaSu39sClUrNlwxS4Tl7a99O+ltFy6yhPMO+wgZuxpQjJ2PZkfrQKmAqrLhw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", + "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", + "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "license": "ISC" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "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/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": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz", + "integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.1", + "@typescript-eslint/parser": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/utils": "8.59.1" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "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/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..d24fe602e51f806ede1e7f7757c605cbe34f85aa --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,42 @@ +{ + "name": "frontend", + "private": true, + "version": "0.7.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@capacitor/android": "^8.3.1", + "@capacitor/cli": "^7.6.2", + "@capacitor/core": "^8.3.1", + "@capacitor/ios": "^8.3.1", + "@supabase/supabase-js": "^2.105.1", + "framer-motion": "^12.38.0", + "i18next": "^26.0.8", + "lucide-react": "^1.14.0", + "react-i18next": "^17.0.6", + "react-router-dom": "^7.1.5", + "@sentry/react": "^8.54.0", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.2", + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.7.0", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^6.4.2" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..6893eb13237060adc0c968a690149a49faa2d7d3 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000000000000000000000000000000000000..e9522193d9f796a9748e9ad8c952a5df73c87db9 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/runtime-config.js b/frontend/public/runtime-config.js new file mode 100644 index 0000000000000000000000000000000000000000..e7418b60147ac96e79948921babf0a95fafffc6a --- /dev/null +++ b/frontend/public/runtime-config.js @@ -0,0 +1 @@ +window.__AUBM_CONFIG__ = {}; diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..f90339d8f765fa2c69d9a341959a8ddb9fff5720 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9eebde60476a86021bf37284425a47b7c506dcc8 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,278 @@ +import React, { useState } from 'react'; +import { + Bot, + LayoutDashboard, + Settings, + PlusCircle, + Menu, + X, + LogOut, + MessageSquare, + ShoppingBag, + Volume2, + Box, + Activity +} from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useAuth } from './context/useAuth'; +import Login from './components/Login'; +import DebateView from './components/DebateView'; +import Marketplace from './components/Marketplace'; +import VoiceControl from './components/VoiceControl'; +import SpatialDashboard from './components/SpatialDashboard'; +import MonitoringView from './components/MonitoringView'; +import NewProject from './components/NewProject'; +import SettingsView from './components/SettingsView'; +import Dashboard from './components/Dashboard'; +import ProjectDetail from './components/ProjectDetail'; +import AgentsView from './components/AgentsView'; +import AgentConsole from './components/AgentConsole'; +import SplashScreen from './components/SplashScreen'; +import { useEffect } from 'react'; +import { getUiMode, saveUiMode } from './services/uiMode'; +import type { UiMode } from './services/uiMode'; +import { getAppVersion } from './services/runtimeConfig'; + +type AppTab = 'dashboard' | 'project-detail' | 'agents' | 'marketplace' | 'debate' | 'voice' | 'spatial' | 'monitoring' | 'new-project' | 'settings'; + +const App: React.FC = () => { + const { session, loading, signOut, profile, user } = useAuth(); + const appVersion = getAppVersion(); + const [activeTab, setActiveTab] = useState('dashboard'); + const [selectedProjectId, setSelectedProjectId] = useState(null); + const [initialTaskId, setInitialTaskId] = useState(null); + const [projectDetailReturnTab, setProjectDetailReturnTab] = useState('dashboard'); + const [uiMode, setUiMode] = useState(() => getUiMode()); + const [isSidebarOpen, setIsSidebarOpen] = useState(() => typeof window === 'undefined' || window.innerWidth >= 900); + const [showSplash, setShowSplash] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => setShowSplash(false), 2500); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + if (uiMode === 'expert') return; + if (['agents', 'marketplace', 'debate', 'voice', 'spatial', 'monitoring'].includes(activeTab)) { + setActiveTab('dashboard'); + } + }, [activeTab, uiMode]); + + const navigateTo = (tab: AppTab) => { + setActiveTab(tab); + if (typeof window !== 'undefined' && window.innerWidth < 900) { + setIsSidebarOpen(false); + } + }; + + const openProjectDetail = (projectId: string, options?: { taskId?: string | null; returnTab?: AppTab }) => { + setSelectedProjectId(projectId); + setInitialTaskId(options?.taskId ?? null); + setProjectDetailReturnTab(options?.returnTab ?? 'dashboard'); + navigateTo('project-detail'); + }; + + const updateUiMode = (mode: UiMode) => { + setUiMode(mode); + saveUiMode(mode); + }; + + if (loading || showSplash) return ; + if (!session) return ; + + return ( +
+ {isSidebarOpen && +
+ + + +
+
+
+ {(profile?.full_name || user?.email || 'U').slice(0, 2).toUpperCase()} +
+
+
{profile?.full_name || user?.email || 'User'}
+
{profile?.role || 'user'}
+
+
+
+ Version {appVersion} +
+
+ + )} + + + {/* Main Content */} +
+
+ + +
+
+ + API Online +
+
+ {uiMode === 'guided' ? 'Guided Mode' : 'Expert Mode'} +
+
+
+ +
+ {activeTab === 'dashboard' && ( + navigateTo('new-project')} + onOpenProject={(projectId) => openProjectDetail(projectId)} + /> + )} + {activeTab === 'project-detail' && selectedProjectId && ( + { + setInitialTaskId(null); + navigateTo(projectDetailReturnTab); + }} + /> + )} + + {activeTab === 'debate' && uiMode === 'expert' && } + {activeTab === 'agents' && uiMode === 'expert' && } + {activeTab === 'marketplace' && uiMode === 'expert' && } + {activeTab === 'voice' && uiMode === 'expert' && } + {activeTab === 'spatial' && uiMode === 'expert' && ( + setSelectedProjectId(projectId)} + onOpenTask={(projectId, taskId) => openProjectDetail(projectId, { taskId, returnTab: 'spatial' })} + /> + )} + {activeTab === 'monitoring' && uiMode === 'expert' && } + {activeTab === 'new-project' && navigateTo('dashboard')} />} + {activeTab === 'settings' && } +
+ + {/* Real-time Agent Console */} + +
+ + ); +}; + +const SidebarItem: React.FC<{ icon: React.ReactNode, label: string, active?: boolean, onClick: () => void }> = ({ icon, label, active, onClick }) => ( + +); + +export default App; diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb Binary files /dev/null and b/frontend/src/assets/hero.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000000000000000000000000000000000000..6c87de9bb3358469122cc991d5cf578927246184 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000000000000000000000000000000000000..5101b674df391399da71c767aa5c976426c9dc7a --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/AgentConsole.tsx b/frontend/src/components/AgentConsole.tsx new file mode 100644 index 0000000000000000000000000000000000000000..aa23804d56ee8c8ecd68d322ab4b08923363a380 --- /dev/null +++ b/frontend/src/components/AgentConsole.tsx @@ -0,0 +1,110 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { Terminal } from 'lucide-react'; +import { supabase } from '../services/supabase'; + +interface LogEntry { + id: string; + created_at: string; + action: string; + content: string; + task_id: string | null; +} + +const AgentConsole: React.FC = () => { + const [logs, setLogs] = useState([]); + const [error, setError] = useState(null); + const scrollRef = useRef(null); + + useEffect(() => { + const fetchLogs = async () => { + const { data, error: supabaseError } = await supabase + .from('agent_logs') + .select('*') + .order('created_at', { ascending: false }) + .limit(50); + + if (supabaseError) { + console.error('Error fetching logs:', supabaseError); + setError(supabaseError.message); + return; + } + + setError(null); + if (data) { + setLogs(data.reverse()); + } + }; + + fetchLogs(); + + // Fallback polling every 3 seconds + const pollInterval = setInterval(fetchLogs, 3000); + + // Set up real-time subscription + const channel = supabase + .channel('agent_logs_changes') + .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'agent_logs' }, (payload) => { + setLogs(prev => { + const newLog = payload.new as LogEntry; + if (prev.some(l => l.id === newLog.id)) return prev; + return [...prev, newLog].slice(-50); + }); + }) + .subscribe(); + + return () => { + clearInterval(pollInterval); + supabase.removeChannel(channel); + }; + }, []); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [logs]); + + const formatTimestamp = (ts: string) => { + const date = new Date(ts); + return date.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); + }; + + return ( +
+
+ + Agent Console +
+
+ {error && ( +
+ [ERROR] {error}. This might be due to Supabase RLS policies. +
+ )} + {logs.length === 0 && !error &&
[System] Waiting for logs...
} + {logs.map((log) => ( +
+ [{formatTimestamp(log.created_at)}] + [{log.action.toUpperCase()}] + {log.content} +
+ ))} +
+
+ ); +}; + +export default AgentConsole; diff --git a/frontend/src/components/AgentsView.tsx b/frontend/src/components/AgentsView.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4d891a7fca9ac2afa5cfa9e7fc8ea2a2d600b37b --- /dev/null +++ b/frontend/src/components/AgentsView.tsx @@ -0,0 +1,220 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Bot, CheckCircle2, PlusCircle, RefreshCw, X } from 'lucide-react'; +import { motion } from 'framer-motion'; +import { supabase } from '../services/supabase'; +import { useAuth } from '../context/useAuth'; +import { getDefaultModel, getDefaultProvider, providerOptions } from '../services/llmConfig'; +import type { SupportedProvider } from '../services/llmConfig'; + +interface Agent { + id: string; + name: string; + role: string | null; + api_provider: SupportedProvider; + model: string; + system_prompt: string | null; + created_at: string; +} + +const AgentsView: React.FC = () => { + const { user } = useAuth(); + const defaultProvider = useMemo(() => getDefaultProvider(), []); + const [agents, setAgents] = useState([]); + const [selectedAgentId, setSelectedAgentId] = useState(null); + const [name, setName] = useState(''); + const [role, setRole] = useState(''); + const [provider, setProvider] = useState(defaultProvider); + const [model, setModel] = useState(getDefaultModel(defaultProvider)); + const [systemPrompt, setSystemPrompt] = useState(''); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + + const providerModels = providerOptions.find((option) => option.id === provider)?.models ?? []; + const isEditing = selectedAgentId !== null; + + const loadAgents = async () => { + setLoading(true); + setError(null); + + const { data, error: selectError } = await supabase + .from('agents') + .select('id,name,role,api_provider,model,system_prompt,created_at') + .order('created_at', { ascending: false }); + + if (selectError) setError(selectError.message); + setAgents((data ?? []) as Agent[]); + setLoading(false); + }; + + useEffect(() => { + loadAgents(); + }, []); + + const handleProviderChange = (value: SupportedProvider) => { + setProvider(value); + setModel(getDefaultModel(value)); + }; + + const resetForm = () => { + setSelectedAgentId(null); + setName(''); + setRole(''); + setProvider(defaultProvider); + setModel(getDefaultModel(defaultProvider)); + setSystemPrompt(''); + }; + + const selectAgent = (agent: Agent) => { + setSelectedAgentId(agent.id); + setName(agent.name); + setRole(agent.role ?? ''); + setProvider(agent.api_provider); + setModel(agent.model); + setSystemPrompt(agent.system_prompt ?? ''); + setMessage(null); + setError(null); + }; + + const saveAgent = async (event: React.FormEvent) => { + event.preventDefault(); + if (!user) { + setError(`You must be signed in to ${isEditing ? 'update' : 'create'} an agent.`); + return; + } + + setSaving(true); + setError(null); + setMessage(null); + + const payload = { + user_id: user.id, + name, + role, + api_provider: provider, + model, + system_prompt: systemPrompt || `You are ${name}, acting as ${role || 'an AI agent'}.` + }; + + const response = isEditing + ? await supabase.from('agents').update(payload).eq('id', selectedAgentId) + : await supabase.from('agents').insert(payload); + + if (response.error) { + setError(response.error.message); + } else { + resetForm(); + setMessage(isEditing ? 'Agent updated successfully.' : 'Agent created successfully.'); + await loadAgents(); + } + + setSaving(false); + }; + + return ( + +
+
+ +
+

Agents

+

Create custom agents and choose the LLM provider used at runtime.

+
+
+ +
+ + {error &&
{error}
} + {message &&
{message}
} + +
+
+
+
+ +

{isEditing ? 'Edit Agent' : 'Create Agent'}

+
+ {isEditing && ( + + )} +
+
+ + +
+ + +
+