diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..78b2d65
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,13 @@
+# Server
+PORT=8080
+HOST=0.0.0.0
+
+# OpenRouter
+OPENROUTER_API_KEY=sk-or-v1-your-key-here
+OPENROUTER_MODEL=deepseek/deepseek-chat
+OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
+
+# App
+DATABASE_URL=sqlite:///./data/assistant.db
+CORS_ORIGINS=http://localhost:5173,http://localhost:8080,http://localhost:3000
+SYSTEM_PROMPT_PATH=./prompts/assistant.md
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3abd722
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+.env
+data/
+__pycache__/
+*.pyc
+.venv/
+venv/
+node_modules/
+dist/
+*.db
+.DS_Store
diff --git a/README.md b/README.md
index 68dcad0..03cc3a5 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,93 @@
-# Home_assistant
+# Home AI Assistant
+Домашний ИИ-ассистент с REST API, веб-интерфейсом и помидоро-таймером. LLM — OpenRouter (по умолчанию DeepSeek).
+
+## Возможности (MVP)
+
+- Чат с потоковыми ответами (SSE)
+- Управление помидоро из чата через tool calling
+- REST API для внешних клиентов (Telegram-бот, мобильное приложение)
+- Веб-морда: вкладки «Чат» и «Помидоро»
+
+## Быстрый старт
+
+### 1. Настройка окружения
+
+```bash
+cp .env.example .env
+```
+
+Заполните в `.env`:
+
+```env
+OPENROUTER_API_KEY=sk-or-v1-...
+PORT=8080
+```
+
+### 2. Запуск через Docker
+
+```bash
+docker compose up --build
+```
+
+- Backend API: http://localhost:8080
+- Web UI: http://localhost:3000
+- Healthcheck: http://localhost:8080/api/v1/health
+
+### 3. Локальная разработка
+
+**Backend:**
+
+```bash
+cd backend
+python -m venv .venv
+.venv\Scripts\activate # Windows
+pip install -r requirements.txt
+uvicorn app.main:app --reload --port 8080
+```
+
+**Frontend:**
+
+```bash
+cd frontend
+npm install
+npm run dev
+```
+
+Vite dev-server: http://localhost:5173 (проксирует `/api` на backend).
+
+## REST API
+
+| Method | Path | Описание |
+|--------|------|----------|
+| GET | `/api/v1/health` | Healthcheck |
+| POST | `/api/v1/chat/sessions` | Создать чат-сессию |
+| GET | `/api/v1/chat/sessions` | Список сессий |
+| GET | `/api/v1/chat/sessions/{id}` | История сообщений |
+| POST | `/api/v1/chat/sessions/{id}/messages` | Отправить сообщение (SSE) |
+| DELETE | `/api/v1/chat/sessions/{id}` | Удалить сессию |
+| GET | `/api/v1/pomodoro/status` | Статус таймера |
+| POST | `/api/v1/pomodoro/start` | Старт `{duration_min, task_note}` |
+| POST | `/api/v1/pomodoro/pause` | Пауза |
+| POST | `/api/v1/pomodoro/resume` | Продолжить |
+| POST | `/api/v1/pomodoro/stop` | Стоп `{result, completed}` |
+| GET | `/api/v1/pomodoro/history` | История сессий |
+
+## Структура проекта
+
+```
+backend/ FastAPI, OpenRouter, SQLite, помидоро
+frontend/ React + Vite, чат и таймер
+data/ SQLite БД (создаётся автоматически)
+```
+
+## Следующие фазы
+
+- Интеграция Taiga + Gitea (project-agent внутри проекта)
+- RAG с Qdrant для документов
+- Проактивные чаты по расписанию
+- Фитнес-трекер
+
+## Модель
+
+По умолчанию: `deepseek/deepseek-chat` через OpenRouter. Альтернатива для болтовни: `google/gemini-2.0-flash`.
diff --git a/backend/.dockerignore b/backend/.dockerignore
new file mode 100644
index 0000000..a514920
--- /dev/null
+++ b/backend/.dockerignore
@@ -0,0 +1,4 @@
+.venv
+__pycache__
+*.pyc
+data/
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..3c7ec74
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,14 @@
+FROM python:3.12-slim
+
+WORKDIR /app
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+RUN mkdir -p /app/data
+
+EXPOSE 8080
+
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
diff --git a/backend/app/__init__.py b/backend/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/api/routes/__init__.py b/backend/app/api/routes/__init__.py
new file mode 100644
index 0000000..4118880
--- /dev/null
+++ b/backend/app/api/routes/__init__.py
@@ -0,0 +1,8 @@
+from fastapi import APIRouter
+
+from app.api.routes import chat, health, pomodoro
+
+api_router = APIRouter(prefix="/api/v1")
+api_router.include_router(health.router, tags=["health"])
+api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
+api_router.include_router(pomodoro.router, prefix="/pomodoro", tags=["pomodoro"])
diff --git a/backend/app/api/routes/chat.py b/backend/app/api/routes/chat.py
new file mode 100644
index 0000000..3587589
--- /dev/null
+++ b/backend/app/api/routes/chat.py
@@ -0,0 +1,55 @@
+from fastapi import APIRouter, Depends, HTTPException
+from fastapi.responses import StreamingResponse
+from sqlalchemy.orm import Session
+
+from app.api.schemas import MessageCreate, MessageOut, SessionCreate, SessionDetailOut, SessionOut
+from app.chat.service import ChatService
+from app.db.base import get_db
+
+router = APIRouter()
+
+
+@router.post("/sessions", response_model=SessionOut)
+def create_session(payload: SessionCreate, db: Session = Depends(get_db)) -> SessionOut:
+ service = ChatService(db)
+ return service.create_session(title=payload.title)
+
+
+@router.get("/sessions", response_model=list[SessionOut])
+def list_sessions(db: Session = Depends(get_db)) -> list[SessionOut]:
+ service = ChatService(db)
+ return service.list_sessions()
+
+
+@router.get("/sessions/{session_id}", response_model=SessionDetailOut)
+def get_session(session_id: int, db: Session = Depends(get_db)) -> SessionDetailOut:
+ service = ChatService(db)
+ session = service.get_session(session_id)
+ if not session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ return session
+
+
+@router.delete("/sessions/{session_id}")
+def delete_session(session_id: int, db: Session = Depends(get_db)) -> dict[str, bool]:
+ service = ChatService(db)
+ if not service.delete_session(session_id):
+ raise HTTPException(status_code=404, detail="Session not found")
+ return {"ok": True}
+
+
+@router.post("/sessions/{session_id}/messages")
+async def send_message(
+ session_id: int,
+ payload: MessageCreate,
+ db: Session = Depends(get_db),
+) -> StreamingResponse:
+ service = ChatService(db)
+ if not service.get_session(session_id):
+ raise HTTPException(status_code=404, detail="Session not found")
+
+ async def event_stream():
+ async for chunk in service.stream_response(session_id, payload.content):
+ yield chunk
+
+ return StreamingResponse(event_stream(), media_type="text/event-stream")
diff --git a/backend/app/api/routes/health.py b/backend/app/api/routes/health.py
new file mode 100644
index 0000000..cccab52
--- /dev/null
+++ b/backend/app/api/routes/health.py
@@ -0,0 +1,8 @@
+from fastapi import APIRouter
+
+router = APIRouter()
+
+
+@router.get("/health")
+def health() -> dict[str, str]:
+ return {"status": "ok"}
diff --git a/backend/app/api/routes/pomodoro.py b/backend/app/api/routes/pomodoro.py
new file mode 100644
index 0000000..c41372b
--- /dev/null
+++ b/backend/app/api/routes/pomodoro.py
@@ -0,0 +1,60 @@
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.orm import Session
+
+from app.api.schemas import PomodoroStart, PomodoroStop
+from app.db.base import get_db
+from app.pomodoro.service import PomodoroService
+
+router = APIRouter()
+
+
+def _handle_value_error(exc: ValueError) -> HTTPException:
+ return HTTPException(status_code=400, detail=str(exc))
+
+
+@router.get("/status")
+def get_status(db: Session = Depends(get_db)) -> dict:
+ return PomodoroService(db).get_status()
+
+
+@router.post("/start")
+def start_pomodoro(payload: PomodoroStart, db: Session = Depends(get_db)) -> dict:
+ try:
+ return PomodoroService(db).start(
+ duration_min=payload.duration_min,
+ task_note=payload.task_note,
+ )
+ except ValueError as exc:
+ raise _handle_value_error(exc) from exc
+
+
+@router.post("/pause")
+def pause_pomodoro(db: Session = Depends(get_db)) -> dict:
+ try:
+ return PomodoroService(db).pause()
+ except ValueError as exc:
+ raise _handle_value_error(exc) from exc
+
+
+@router.post("/resume")
+def resume_pomodoro(db: Session = Depends(get_db)) -> dict:
+ try:
+ return PomodoroService(db).resume()
+ except ValueError as exc:
+ raise _handle_value_error(exc) from exc
+
+
+@router.post("/stop")
+def stop_pomodoro(payload: PomodoroStop, db: Session = Depends(get_db)) -> dict:
+ try:
+ return PomodoroService(db).stop(
+ result=payload.result,
+ completed=payload.completed,
+ )
+ except ValueError as exc:
+ raise _handle_value_error(exc) from exc
+
+
+@router.get("/history")
+def get_history(limit: int = 20, db: Session = Depends(get_db)) -> list[dict]:
+ return PomodoroService(db).history(limit=limit)
diff --git a/backend/app/api/schemas.py b/backend/app/api/schemas.py
new file mode 100644
index 0000000..798a205
--- /dev/null
+++ b/backend/app/api/schemas.py
@@ -0,0 +1,43 @@
+from datetime import datetime
+
+from pydantic import BaseModel, Field
+
+
+class SessionCreate(BaseModel):
+ title: str = "Новый чат"
+
+
+class SessionOut(BaseModel):
+ id: int
+ title: str
+ created_at: datetime
+ updated_at: datetime
+
+ model_config = {"from_attributes": True}
+
+
+class MessageOut(BaseModel):
+ id: int
+ role: str
+ content: str
+ created_at: datetime
+
+ model_config = {"from_attributes": True}
+
+
+class SessionDetailOut(SessionOut):
+ messages: list[MessageOut]
+
+
+class MessageCreate(BaseModel):
+ content: str = Field(min_length=1)
+
+
+class PomodoroStart(BaseModel):
+ duration_min: int = Field(default=25, ge=1, le=180)
+ task_note: str = ""
+
+
+class PomodoroStop(BaseModel):
+ result: str = ""
+ completed: bool = False
diff --git a/backend/app/chat/__init__.py b/backend/app/chat/__init__.py
new file mode 100644
index 0000000..144897c
--- /dev/null
+++ b/backend/app/chat/__init__.py
@@ -0,0 +1,3 @@
+from app.chat.service import ChatService
+
+__all__ = ["ChatService"]
diff --git a/backend/app/chat/service.py b/backend/app/chat/service.py
new file mode 100644
index 0000000..298ec91
--- /dev/null
+++ b/backend/app/chat/service.py
@@ -0,0 +1,141 @@
+import json
+from collections.abc import AsyncIterator
+from typing import Any
+
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from app.config import get_settings
+from app.db.models import ChatSession, Message
+from app.llm.client import LLMClient
+from app.tools.registry import TOOL_DEFINITIONS, execute_tool
+
+MAX_TOOL_ROUNDS = 5
+
+
+class ChatService:
+ def __init__(self, db: Session):
+ self.db = db
+ self.llm = LLMClient()
+ self.system_prompt = get_settings().load_system_prompt()
+
+ def list_sessions(self) -> list[ChatSession]:
+ stmt = select(ChatSession).order_by(ChatSession.updated_at.desc())
+ return list(self.db.scalars(stmt).all())
+
+ def get_session(self, session_id: int) -> ChatSession | None:
+ return self.db.get(ChatSession, session_id)
+
+ def create_session(self, title: str = "Новый чат") -> ChatSession:
+ session = ChatSession(title=title)
+ self.db.add(session)
+ self.db.commit()
+ self.db.refresh(session)
+ return session
+
+ def delete_session(self, session_id: int) -> bool:
+ session = self.get_session(session_id)
+ if not session:
+ return False
+ self.db.delete(session)
+ self.db.commit()
+ return True
+
+ def _build_messages(self, session: ChatSession) -> list[dict[str, Any]]:
+ messages: list[dict[str, Any]] = [{"role": "system", "content": self.system_prompt}]
+ for msg in session.messages:
+ content = msg.content or None
+ entry: dict[str, Any] = {"role": msg.role, "content": content}
+ if msg.tool_calls_json:
+ entry["tool_calls"] = json.loads(msg.tool_calls_json)
+ if not content:
+ entry["content"] = None
+ if msg.role == "tool" and msg.tool_call_id:
+ entry["tool_call_id"] = msg.tool_call_id
+ messages.append(entry)
+ return messages
+
+ def _save_message(
+ self,
+ session_id: int,
+ role: str,
+ content: str = "",
+ tool_calls: list[dict[str, Any]] | None = None,
+ tool_call_id: str | None = None,
+ ) -> Message:
+ message = Message(
+ session_id=session_id,
+ role=role,
+ content=content,
+ tool_calls_json=json.dumps(tool_calls, ensure_ascii=False) if tool_calls else None,
+ tool_call_id=tool_call_id,
+ )
+ self.db.add(message)
+ session = self.get_session(session_id)
+ if session and role == "user" and session.title == "Новый чат" and content:
+ session.title = content[:60] + ("..." if len(content) > 60 else "")
+ self.db.commit()
+ self.db.refresh(message)
+ return message
+
+ async def stream_response(self, session_id: int, user_text: str) -> AsyncIterator[str]:
+ session = self.get_session(session_id)
+ if not session:
+ yield self._sse("error", {"message": "Session not found"})
+ return
+
+ self._save_message(session_id, "user", user_text)
+ messages = self._build_messages(session)
+
+ for _ in range(MAX_TOOL_ROUNDS):
+ content_parts: list[str] = []
+ tool_calls: list[dict[str, Any]] = []
+
+ async for event in self.llm.stream_chat(messages, tools=TOOL_DEFINITIONS):
+ if event["type"] == "content":
+ content_parts.append(event["content"])
+ yield self._sse("token", {"content": event["content"]})
+ elif event["type"] == "tool_calls":
+ tool_calls = event["tool_calls"]
+
+ if tool_calls:
+ assistant_msg: dict[str, Any] = {
+ "role": "assistant",
+ "content": "".join(content_parts) or None,
+ "tool_calls": tool_calls,
+ }
+ messages.append(assistant_msg)
+ self._save_message(
+ session_id,
+ "assistant",
+ "".join(content_parts),
+ tool_calls=tool_calls,
+ )
+
+ for tool_call in tool_calls:
+ fn = tool_call["function"]
+ args = LLMClient.parse_tool_arguments(fn.get("arguments", ""))
+ result = execute_tool(self.db, fn["name"], args)
+ tool_message = {
+ "role": "tool",
+ "tool_call_id": tool_call["id"],
+ "content": result,
+ }
+ messages.append(tool_message)
+ self._save_message(session_id, "tool", result, tool_call_id=tool_call["id"])
+ yield self._sse("tool", {"name": fn["name"], "result": json.loads(result)})
+
+ continue
+
+ final_content = "".join(content_parts)
+ if final_content:
+ self._save_message(session_id, "assistant", final_content)
+
+ yield self._sse("done", {})
+ return
+
+ yield self._sse("error", {"message": "Too many tool call rounds"})
+
+ @staticmethod
+ def _sse(event: str, data: dict[str, Any]) -> str:
+ return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
diff --git a/backend/app/config.py b/backend/app/config.py
new file mode 100644
index 0000000..49e8399
--- /dev/null
+++ b/backend/app/config.py
@@ -0,0 +1,38 @@
+from functools import lru_cache
+from pathlib import Path
+
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class Settings(BaseSettings):
+ model_config = SettingsConfigDict(
+ env_file=(".env", "../.env"),
+ env_file_encoding="utf-8",
+ extra="ignore",
+ )
+
+ host: str = "0.0.0.0"
+ port: int = 8080
+
+ openrouter_api_key: str = ""
+ openrouter_model: str = "deepseek/deepseek-chat"
+ openrouter_base_url: str = "https://openrouter.ai/api/v1"
+
+ database_url: str = "sqlite:///./data/assistant.db"
+ cors_origins: str = "http://localhost:5173,http://localhost:8080,http://localhost:3000"
+ system_prompt_path: str = "./prompts/assistant.md"
+
+ @property
+ def cors_origins_list(self) -> list[str]:
+ return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()]
+
+ def load_system_prompt(self) -> str:
+ path = Path(self.system_prompt_path)
+ if path.is_file():
+ return path.read_text(encoding="utf-8")
+ return "Ты домашний ИИ-ассистент. Общайся на русском."
+
+
+@lru_cache
+def get_settings() -> Settings:
+ return Settings()
diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py
new file mode 100644
index 0000000..3620f9a
--- /dev/null
+++ b/backend/app/db/__init__.py
@@ -0,0 +1,11 @@
+from app.db.base import Base, get_db, init_db
+from app.db.models import ChatSession, Message, PomodoroSession
+
+__all__ = [
+ "Base",
+ "ChatSession",
+ "Message",
+ "PomodoroSession",
+ "get_db",
+ "init_db",
+]
diff --git a/backend/app/db/base.py b/backend/app/db/base.py
new file mode 100644
index 0000000..0340449
--- /dev/null
+++ b/backend/app/db/base.py
@@ -0,0 +1,39 @@
+from collections.abc import Generator
+from pathlib import Path
+
+from sqlalchemy import create_engine
+from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
+
+from app.config import get_settings
+
+
+class Base(DeclarativeBase):
+ pass
+
+
+def _ensure_sqlite_dir(database_url: str) -> None:
+ if database_url.startswith("sqlite:///"):
+ db_path = database_url.replace("sqlite:///", "", 1)
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
+
+
+settings = get_settings()
+_ensure_sqlite_dir(settings.database_url)
+
+connect_args = {"check_same_thread": False} if settings.database_url.startswith("sqlite") else {}
+engine = create_engine(settings.database_url, connect_args=connect_args)
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+
+def init_db() -> None:
+ from app.db import models # noqa: F401
+
+ Base.metadata.create_all(bind=engine)
+
+
+def get_db() -> Generator[Session, None, None]:
+ db = SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
diff --git a/backend/app/db/models.py b/backend/app/db/models.py
new file mode 100644
index 0000000..d33d6a8
--- /dev/null
+++ b/backend/app/db/models.py
@@ -0,0 +1,51 @@
+from datetime import datetime
+
+from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from app.db.base import Base
+
+
+class ChatSession(Base):
+ __tablename__ = "chat_sessions"
+
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
+ title: Mapped[str] = mapped_column(String(255), default="Новый чат")
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+ updated_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
+ )
+
+ messages: Mapped[list["Message"]] = relationship(
+ back_populates="session", cascade="all, delete-orphan", order_by="Message.created_at"
+ )
+
+
+class Message(Base):
+ __tablename__ = "messages"
+
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
+ session_id: Mapped[int] = mapped_column(ForeignKey("chat_sessions.id", ondelete="CASCADE"), index=True)
+ role: Mapped[str] = mapped_column(String(32))
+ content: Mapped[str] = mapped_column(Text, default="")
+ tool_calls_json: Mapped[str | None] = mapped_column(Text, nullable=True)
+ tool_call_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+
+ session: Mapped["ChatSession"] = relationship(back_populates="messages")
+
+
+class PomodoroSession(Base):
+ __tablename__ = "pomodoro_sessions"
+
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
+ status: Mapped[str] = mapped_column(String(32), default="idle")
+ duration_min: Mapped[int] = mapped_column(Integer, default=25)
+ task_note: Mapped[str] = mapped_column(Text, default="")
+ result: Mapped[str | None] = mapped_column(Text, nullable=True)
+ completed: Mapped[bool] = mapped_column(Boolean, default=False)
+ started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
+ paused_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
+ elapsed_seconds: Mapped[int] = mapped_column(Integer, default=0)
+ finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
diff --git a/backend/app/llm/__init__.py b/backend/app/llm/__init__.py
new file mode 100644
index 0000000..d554786
--- /dev/null
+++ b/backend/app/llm/__init__.py
@@ -0,0 +1,3 @@
+from app.llm.client import LLMClient
+
+__all__ = ["LLMClient"]
diff --git a/backend/app/llm/client.py b/backend/app/llm/client.py
new file mode 100644
index 0000000..3be0b6f
--- /dev/null
+++ b/backend/app/llm/client.py
@@ -0,0 +1,112 @@
+import json
+from collections.abc import AsyncIterator
+from typing import Any
+
+from openai import AsyncOpenAI
+
+from app.config import get_settings
+
+
+class LLMClient:
+ def __init__(self) -> None:
+ settings = get_settings()
+ self.model = settings.openrouter_model
+ self.client = AsyncOpenAI(
+ api_key=settings.openrouter_api_key,
+ base_url=settings.openrouter_base_url,
+ )
+
+ async def stream_chat(
+ self,
+ messages: list[dict[str, Any]],
+ tools: list[dict[str, Any]] | None = None,
+ ) -> AsyncIterator[dict[str, Any]]:
+ kwargs: dict[str, Any] = {
+ "model": self.model,
+ "messages": messages,
+ "stream": True,
+ "temperature": 0.7,
+ }
+ if tools:
+ kwargs["tools"] = tools
+
+ stream = await self.client.chat.completions.create(**kwargs)
+
+ tool_calls: dict[int, dict[str, Any]] = {}
+
+ async for chunk in stream:
+ if not chunk.choices:
+ continue
+
+ choice = chunk.choices[0]
+ delta = choice.delta
+
+ if delta.content:
+ yield {"type": "content", "content": delta.content}
+
+ if delta.tool_calls:
+ for tool_call in delta.tool_calls:
+ idx = tool_call.index
+ if idx not in tool_calls:
+ tool_calls[idx] = {
+ "id": tool_call.id or "",
+ "type": "function",
+ "function": {"name": "", "arguments": ""},
+ }
+ if tool_call.id:
+ tool_calls[idx]["id"] = tool_call.id
+ if tool_call.function:
+ if tool_call.function.name:
+ tool_calls[idx]["function"]["name"] = tool_call.function.name
+ if tool_call.function.arguments:
+ tool_calls[idx]["function"]["arguments"] += tool_call.function.arguments
+
+ if choice.finish_reason:
+ if tool_calls:
+ yield {"type": "tool_calls", "tool_calls": list(tool_calls.values())}
+ yield {"type": "done", "finish_reason": choice.finish_reason}
+
+ async def complete(
+ self,
+ messages: list[dict[str, Any]],
+ tools: list[dict[str, Any]] | None = None,
+ ) -> dict[str, Any]:
+ kwargs: dict[str, Any] = {
+ "model": self.model,
+ "messages": messages,
+ "temperature": 0.7,
+ }
+ if tools:
+ kwargs["tools"] = tools
+
+ response = await self.client.chat.completions.create(**kwargs)
+ message = response.choices[0].message
+
+ result: dict[str, Any] = {
+ "content": message.content or "",
+ "tool_calls": [],
+ }
+
+ if message.tool_calls:
+ result["tool_calls"] = [
+ {
+ "id": tc.id,
+ "type": "function",
+ "function": {
+ "name": tc.function.name,
+ "arguments": tc.function.arguments,
+ },
+ }
+ for tc in message.tool_calls
+ ]
+
+ return result
+
+ @staticmethod
+ def parse_tool_arguments(arguments: str) -> dict[str, Any]:
+ if not arguments:
+ return {}
+ try:
+ return json.loads(arguments)
+ except json.JSONDecodeError:
+ return {}
diff --git a/backend/app/main.py b/backend/app/main.py
new file mode 100644
index 0000000..6ca6284
--- /dev/null
+++ b/backend/app/main.py
@@ -0,0 +1,33 @@
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from app.api.routes import api_router
+from app.config import get_settings
+from app.db.base import init_db
+
+
+@asynccontextmanager
+async def lifespan(_: FastAPI):
+ init_db()
+ yield
+
+
+def create_app() -> FastAPI:
+ settings = get_settings()
+ app = FastAPI(title="Home AI Assistant", lifespan=lifespan)
+
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=settings.cors_origins_list,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+
+ app.include_router(api_router)
+ return app
+
+
+app = create_app()
diff --git a/backend/app/pomodoro/__init__.py b/backend/app/pomodoro/__init__.py
new file mode 100644
index 0000000..4c5e306
--- /dev/null
+++ b/backend/app/pomodoro/__init__.py
@@ -0,0 +1,3 @@
+from app.pomodoro.service import PomodoroService
+
+__all__ = ["PomodoroService"]
diff --git a/backend/app/pomodoro/service.py b/backend/app/pomodoro/service.py
new file mode 100644
index 0000000..3184656
--- /dev/null
+++ b/backend/app/pomodoro/service.py
@@ -0,0 +1,152 @@
+from datetime import datetime, timezone
+
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from app.db.models import PomodoroSession
+
+
+def _utcnow() -> datetime:
+ return datetime.now(timezone.utc)
+
+
+class PomodoroService:
+ def __init__(self, db: Session):
+ self.db = db
+
+ def _get_active(self) -> PomodoroSession | None:
+ stmt = (
+ select(PomodoroSession)
+ .where(PomodoroSession.status.in_(("running", "paused")))
+ .order_by(PomodoroSession.id.desc())
+ .limit(1)
+ )
+ return self.db.scalar(stmt)
+
+ def _elapsed(self, session: PomodoroSession) -> int:
+ elapsed = session.elapsed_seconds
+ if session.status == "running" and session.started_at:
+ started = session.started_at
+ if started.tzinfo is None:
+ started = started.replace(tzinfo=timezone.utc)
+ delta = _utcnow() - started
+ elapsed += int(delta.total_seconds())
+ return elapsed
+
+ def _remaining(self, session: PomodoroSession) -> int:
+ total = session.duration_min * 60
+ return max(0, total - self._elapsed(session))
+
+ def _to_status_dict(self, session: PomodoroSession | None) -> dict:
+ if not session:
+ return {
+ "status": "idle",
+ "duration_min": 25,
+ "task_note": "",
+ "elapsed_seconds": 0,
+ "remaining_seconds": 0,
+ "session_id": None,
+ }
+
+ elapsed = self._elapsed(session)
+ total = session.duration_min * 60
+ remaining = max(0, total - elapsed)
+
+ if session.status == "running" and remaining == 0:
+ session.status = "completed"
+ session.finished_at = _utcnow()
+ session.completed = True
+ self.db.commit()
+ self.db.refresh(session)
+
+ return {
+ "status": session.status,
+ "duration_min": session.duration_min,
+ "task_note": session.task_note,
+ "elapsed_seconds": elapsed,
+ "remaining_seconds": remaining,
+ "session_id": session.id,
+ "started_at": session.started_at.isoformat() if session.started_at else None,
+ "finished_at": session.finished_at.isoformat() if session.finished_at else None,
+ }
+
+ def get_status(self) -> dict:
+ return self._to_status_dict(self._get_active())
+
+ def start(self, duration_min: int = 25, task_note: str = "") -> dict:
+ active = self._get_active()
+ if active:
+ raise ValueError("Таймер уже запущен. Сначала остановите текущую сессию.")
+
+ session = PomodoroSession(
+ status="running",
+ duration_min=duration_min,
+ task_note=task_note,
+ started_at=_utcnow(),
+ )
+ self.db.add(session)
+ self.db.commit()
+ self.db.refresh(session)
+ return self._to_status_dict(session)
+
+ def pause(self) -> dict:
+ session = self._get_active()
+ if not session or session.status != "running":
+ raise ValueError("Нет активного запущенного таймера.")
+
+ session.elapsed_seconds = self._elapsed(session)
+ session.status = "paused"
+ session.paused_at = _utcnow()
+ session.started_at = None
+ self.db.commit()
+ self.db.refresh(session)
+ return self._to_status_dict(session)
+
+ def resume(self) -> dict:
+ session = self._get_active()
+ if not session or session.status != "paused":
+ raise ValueError("Нет таймера на паузе.")
+
+ session.status = "running"
+ session.started_at = _utcnow()
+ session.paused_at = None
+ self.db.commit()
+ self.db.refresh(session)
+ return self._to_status_dict(session)
+
+ def stop(self, result: str = "", completed: bool = False) -> dict:
+ session = self._get_active()
+ if not session:
+ raise ValueError("Нет активного таймера.")
+
+ session.elapsed_seconds = self._elapsed(session)
+ session.status = "completed" if completed else "cancelled"
+ session.result = result
+ session.completed = completed
+ session.finished_at = _utcnow()
+ session.started_at = None
+ self.db.commit()
+ self.db.refresh(session)
+ return self._to_status_dict(session)
+
+ def history(self, limit: int = 20) -> list[dict]:
+ stmt = (
+ select(PomodoroSession)
+ .where(PomodoroSession.status.in_(("completed", "cancelled")))
+ .order_by(PomodoroSession.finished_at.desc())
+ .limit(limit)
+ )
+ sessions = self.db.scalars(stmt).all()
+ return [
+ {
+ "id": s.id,
+ "status": s.status,
+ "duration_min": s.duration_min,
+ "task_note": s.task_note,
+ "result": s.result,
+ "completed": s.completed,
+ "elapsed_seconds": s.elapsed_seconds,
+ "finished_at": s.finished_at.isoformat() if s.finished_at else None,
+ }
+ for s in sessions
+ ]
diff --git a/backend/app/tools/__init__.py b/backend/app/tools/__init__.py
new file mode 100644
index 0000000..8ee19cf
--- /dev/null
+++ b/backend/app/tools/__init__.py
@@ -0,0 +1,3 @@
+from app.tools.registry import TOOL_DEFINITIONS, execute_tool
+
+__all__ = ["TOOL_DEFINITIONS", "execute_tool"]
diff --git a/backend/app/tools/registry.py b/backend/app/tools/registry.py
new file mode 100644
index 0000000..95c6958
--- /dev/null
+++ b/backend/app/tools/registry.py
@@ -0,0 +1,102 @@
+import json
+from typing import Any
+
+from sqlalchemy.orm import Session
+
+from app.pomodoro.service import PomodoroService
+
+TOOL_DEFINITIONS: list[dict[str, Any]] = [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_pomodoro_status",
+ "description": "Получить текущий статус помидоро-таймера",
+ "parameters": {"type": "object", "properties": {}, "required": []},
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "start_pomodoro",
+ "description": "Запустить помидоро-таймер",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "duration_min": {
+ "type": "integer",
+ "description": "Длительность в минутах, по умолчанию 25",
+ },
+ "task_note": {
+ "type": "string",
+ "description": "Над чем работаем в этой сессии",
+ },
+ },
+ "required": [],
+ },
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "stop_pomodoro",
+ "description": "Остановить текущий помидоро-таймер",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "type": "string",
+ "description": "Краткий отчёт о том, что сделано",
+ },
+ "completed": {
+ "type": "boolean",
+ "description": "True если задача полностью завершена",
+ },
+ },
+ "required": [],
+ },
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "get_pomodoro_history",
+ "description": "Получить историю завершённых помидоро-сессий",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "integer",
+ "description": "Сколько последних сессий вернуть, по умолчанию 10",
+ }
+ },
+ "required": [],
+ },
+ },
+ },
+]
+
+
+def execute_tool(db: Session, name: str, arguments: dict[str, Any]) -> str:
+ service = PomodoroService(db)
+
+ try:
+ if name == "get_pomodoro_status":
+ result = service.get_status()
+ elif name == "start_pomodoro":
+ result = service.start(
+ duration_min=arguments.get("duration_min", 25),
+ task_note=arguments.get("task_note", ""),
+ )
+ elif name == "stop_pomodoro":
+ result = service.stop(
+ result=arguments.get("result", ""),
+ completed=arguments.get("completed", False),
+ )
+ elif name == "get_pomodoro_history":
+ result = service.history(limit=arguments.get("limit", 10))
+ else:
+ return json.dumps({"error": f"Unknown tool: {name}"}, ensure_ascii=False)
+
+ return json.dumps(result, ensure_ascii=False)
+ except ValueError as exc:
+ return json.dumps({"error": str(exc)}, ensure_ascii=False)
diff --git a/backend/prompts/assistant.md b/backend/prompts/assistant.md
new file mode 100644
index 0000000..8691583
--- /dev/null
+++ b/backend/prompts/assistant.md
@@ -0,0 +1,13 @@
+Ты домашний ИИ-ассистент. Общайся на русском языке.
+
+Твои задачи:
+- Отвечать на вопросы и давать практичные советы
+- Помогать с помидоро-таймером: запускать, останавливать, проверять статус, смотреть историю
+- Быть дружелюбным, кратким и по делу
+
+Когда пользователь просит завести помидор, используй инструмент start_pomodoro.
+Когда спрашивает статус таймера — get_pomodoro_status.
+Когда хочет остановить — stop_pomodoro.
+Когда спрашивает что делал — get_pomodoro_history.
+
+Не выдумывай данные о таймере — всегда используй инструменты.
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 0000000..20213c3
--- /dev/null
+++ b/backend/requirements.txt
@@ -0,0 +1,7 @@
+fastapi>=0.115.0
+uvicorn[standard]>=0.32.0
+sqlalchemy>=2.0.36
+pydantic-settings>=2.6.0
+openai>=1.55.0
+python-dotenv>=1.0.1
+aiosqlite>=0.20.0
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..f156f34
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,20 @@
+services:
+ backend:
+ build: ./backend
+ ports:
+ - "${PORT:-8080}:8080"
+ env_file: .env
+ volumes:
+ - ./data:/app/data
+ restart: unless-stopped
+
+ frontend:
+ build:
+ context: ./frontend
+ args:
+ VITE_API_URL: ""
+ ports:
+ - "3000:80"
+ depends_on:
+ - backend
+ restart: unless-stopped
diff --git a/frontend/.dockerignore b/frontend/.dockerignore
new file mode 100644
index 0000000..f06235c
--- /dev/null
+++ b/frontend/.dockerignore
@@ -0,0 +1,2 @@
+node_modules
+dist
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..1d354b0
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,19 @@
+FROM node:20-alpine AS build
+
+WORKDIR /app
+
+ARG VITE_API_URL=
+ENV VITE_API_URL=$VITE_API_URL
+
+COPY package.json ./
+RUN npm install
+
+COPY . .
+RUN npm run build
+
+FROM nginx:alpine
+
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=build /app/dist /usr/share/nginx/html
+
+EXPOSE 80
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..b0d097d
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Home AI Assistant
+
+
+
+
+
+
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
new file mode 100644
index 0000000..f647f0f
--- /dev/null
+++ b/frontend/nginx.conf
@@ -0,0 +1,20 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location /api/ {
+ proxy_pass http://backend:8080;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_buffering off;
+ proxy_cache off;
+ chunked_transfer_encoding off;
+ }
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..a104984
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,2943 @@
+{
+ "name": "home-assistant-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "home-assistant-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-markdown": "^9.0.1",
+ "react-router-dom": "^6.28.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.3",
+ "typescript": "^5.6.3",
+ "vite": "^5.4.11"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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/@remix-run/router": {
+ "version": "1.23.3",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
+ "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz",
+ "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz",
+ "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz",
+ "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz",
+ "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz",
+ "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz",
+ "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz",
+ "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz",
+ "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz",
+ "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz",
+ "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz",
+ "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz",
+ "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz",
+ "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz",
+ "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz",
+ "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz",
+ "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz",
+ "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz",
+ "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz",
+ "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz",
+ "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz",
+ "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz",
+ "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz",
+ "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz",
+ "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz",
+ "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "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/debug": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.31",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+ "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/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
+ "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.34",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz",
+ "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "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/caniuse-lite": {
+ "version": "1.0.30001797",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz",
+ "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/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/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/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/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.369",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.369.tgz",
+ "integrity": "sha512-XM22K9FNaaCOvMMrBn1caIc8v0g6+pKt660ZbfQqUZvfil0hEzr8ZoiY7VcSLGM3L/x3rz5PqZrk+bKOOmVM9w==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/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/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/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/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/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/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/mdast-util-from-markdown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/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/node-releases": {
+ "version": "2.0.47",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/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/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
+ "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/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-markdown": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz",
+ "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.4",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
+ "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.4",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
+ "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.3",
+ "react-router": "6.30.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz",
+ "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.61.1",
+ "@rollup/rollup-android-arm64": "4.61.1",
+ "@rollup/rollup-darwin-arm64": "4.61.1",
+ "@rollup/rollup-darwin-x64": "4.61.1",
+ "@rollup/rollup-freebsd-arm64": "4.61.1",
+ "@rollup/rollup-freebsd-x64": "4.61.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.61.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.61.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.61.1",
+ "@rollup/rollup-linux-arm64-musl": "4.61.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.61.1",
+ "@rollup/rollup-linux-loong64-musl": "4.61.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.61.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.61.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.61.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.61.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.61.1",
+ "@rollup/rollup-linux-x64-gnu": "4.61.1",
+ "@rollup/rollup-linux-x64-musl": "4.61.1",
+ "@rollup/rollup-openbsd-x64": "4.61.1",
+ "@rollup/rollup-openharmony-arm64": "4.61.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.61.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.61.1",
+ "@rollup/rollup-win32-x64-gnu": "4.61.1",
+ "@rollup/rollup-win32-x64-msvc": "4.61.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "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/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.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",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "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/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..38d8a2d
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "home-assistant-frontend",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-markdown": "^9.0.1",
+ "react-router-dom": "^6.28.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.3",
+ "typescript": "^5.6.3",
+ "vite": "^5.4.11"
+ }
+}
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..398dbbc
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,41 @@
+.app {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.app-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid #2a2f3a;
+ background: #151922;
+}
+
+.app-header h1 {
+ margin: 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+}
+
+.app-header nav {
+ display: flex;
+ gap: 0.75rem;
+}
+
+.app-header nav a {
+ padding: 0.45rem 0.9rem;
+ border-radius: 8px;
+ color: #a8b0bd;
+}
+
+.app-header nav a.active {
+ background: #2b3445;
+ color: #fff;
+}
+
+.app-main {
+ flex: 1;
+ min-height: 0;
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..852ee06
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,26 @@
+import { NavLink, Route, Routes } from "react-router-dom";
+import Chat from "./pages/Chat";
+import Pomodoro from "./pages/Pomodoro";
+import "./App.css";
+
+export default function App() {
+ return (
+
+
+ Home AI Assistant
+
+
+ Чат
+
+ Помидоро
+
+
+
+
+ } />
+ } />
+
+
+
+ );
+}
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
new file mode 100644
index 0000000..e55fcdf
--- /dev/null
+++ b/frontend/src/api/client.ts
@@ -0,0 +1,132 @@
+const API_BASE = import.meta.env.VITE_API_URL ?? "";
+
+export interface ChatSession {
+ id: number;
+ title: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface ChatMessage {
+ id: number;
+ role: string;
+ content: string;
+ created_at: string;
+}
+
+export interface SessionDetail extends ChatSession {
+ messages: ChatMessage[];
+}
+
+export interface PomodoroStatus {
+ status: string;
+ duration_min: number;
+ task_note: string;
+ elapsed_seconds: number;
+ remaining_seconds: number;
+ session_id: number | null;
+ started_at?: string | null;
+ finished_at?: string | null;
+}
+
+export interface PomodoroHistoryItem {
+ id: number;
+ status: string;
+ duration_min: number;
+ task_note: string;
+ result: string | null;
+ completed: boolean;
+ elapsed_seconds: number;
+ finished_at: string | null;
+}
+
+async function request(path: string, options?: RequestInit): Promise {
+ const response = await fetch(`${API_BASE}${path}`, options);
+ if (!response.ok) {
+ const text = await response.text();
+ throw new Error(text || response.statusText);
+ }
+ return response.json() as Promise;
+}
+
+export const api = {
+ health: () => request<{ status: string }>("/api/v1/health"),
+
+ listSessions: () => request("/api/v1/chat/sessions"),
+
+ createSession: (title = "Новый чат") =>
+ request("/api/v1/chat/sessions", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ title }),
+ }),
+
+ getSession: (id: number) => request(`/api/v1/chat/sessions/${id}`),
+
+ deleteSession: (id: number) =>
+ request<{ ok: boolean }>(`/api/v1/chat/sessions/${id}`, { method: "DELETE" }),
+
+ sendMessage: async function* (sessionId: number, content: string) {
+ const response = await fetch(`${API_BASE}/api/v1/chat/sessions/${sessionId}/messages`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ content }),
+ });
+
+ if (!response.ok || !response.body) {
+ throw new Error("Failed to send message");
+ }
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = "";
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ buffer += decoder.decode(value, { stream: true });
+ const parts = buffer.split("\n\n");
+ buffer = parts.pop() ?? "";
+
+ for (const part of parts) {
+ const lines = part.split("\n");
+ let event = "message";
+ let data = "";
+
+ for (const line of lines) {
+ if (line.startsWith("event: ")) event = line.slice(7);
+ if (line.startsWith("data: ")) data = line.slice(6);
+ }
+
+ if (data) {
+ yield { event, data: JSON.parse(data) };
+ }
+ }
+ }
+ },
+
+ pomodoroStatus: () => request("/api/v1/pomodoro/status"),
+
+ pomodoroStart: (duration_min: number, task_note: string) =>
+ request("/api/v1/pomodoro/start", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ duration_min, task_note }),
+ }),
+
+ pomodoroPause: () =>
+ request("/api/v1/pomodoro/pause", { method: "POST" }),
+
+ pomodoroResume: () =>
+ request("/api/v1/pomodoro/resume", { method: "POST" }),
+
+ pomodoroStop: (result: string, completed: boolean) =>
+ request("/api/v1/pomodoro/stop", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ result, completed }),
+ }),
+
+ pomodoroHistory: () => request("/api/v1/pomodoro/history"),
+};
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..75f1399
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,32 @@
+:root {
+ color-scheme: dark;
+ font-family: Inter, system-ui, -apple-system, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+ color: #e8eaed;
+ background-color: #0f1115;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+}
+
+button,
+input,
+textarea {
+ font: inherit;
+}
+
+button {
+ cursor: pointer;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..fe05248
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { BrowserRouter } from "react-router-dom";
+import App from "./App";
+import "./index.css";
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+);
diff --git a/frontend/src/pages/Chat.css b/frontend/src/pages/Chat.css
new file mode 100644
index 0000000..ba49a98
--- /dev/null
+++ b/frontend/src/pages/Chat.css
@@ -0,0 +1,156 @@
+.chat-layout {
+ display: grid;
+ grid-template-columns: 280px 1fr;
+ height: calc(100vh - 65px);
+}
+
+.chat-sidebar {
+ border-right: 1px solid #2a2f3a;
+ padding: 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ background: #12151c;
+}
+
+.primary-btn {
+ background: #4f7cff;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ padding: 0.65rem 1rem;
+}
+
+.session-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+.session-list li {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ border-radius: 8px;
+}
+
+.session-list li.active {
+ background: #1f2633;
+}
+
+.session-list li button:first-child {
+ flex: 1;
+ text-align: left;
+ background: transparent;
+ border: none;
+ color: inherit;
+ padding: 0.55rem 0.7rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.delete-btn {
+ background: transparent;
+ border: none;
+ color: #7d8796;
+ padding: 0.3rem 0.5rem;
+}
+
+.chat-main {
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+.chat-empty {
+ margin: auto;
+ color: #7d8796;
+}
+
+.messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.message {
+ max-width: 80%;
+ padding: 0.9rem 1rem;
+ border-radius: 12px;
+}
+
+.message-user {
+ align-self: flex-end;
+ background: #2b4acb;
+}
+
+.message-assistant,
+.message-tool {
+ align-self: flex-start;
+ background: #1b2130;
+ border: 1px solid #2a3142;
+}
+
+.message-role {
+ font-size: 0.75rem;
+ color: #8b95a5;
+ margin-bottom: 0.35rem;
+ text-transform: uppercase;
+}
+
+.message-content p {
+ margin: 0 0 0.5rem;
+}
+
+.message-content p:last-child {
+ margin-bottom: 0;
+}
+
+.chat-input {
+ display: flex;
+ gap: 0.75rem;
+ padding: 1rem 1.5rem;
+ border-top: 1px solid #2a2f3a;
+}
+
+.chat-input textarea {
+ flex: 1;
+ resize: none;
+ border-radius: 10px;
+ border: 1px solid #2f3748;
+ background: #12151c;
+ color: inherit;
+ padding: 0.75rem 1rem;
+}
+
+.chat-input button {
+ align-self: flex-end;
+ background: #4f7cff;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ padding: 0.65rem 1.2rem;
+}
+
+.chat-input button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+@media (max-width: 768px) {
+ .chat-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .chat-sidebar {
+ display: none;
+ }
+}
diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx
new file mode 100644
index 0000000..93b9dcc
--- /dev/null
+++ b/frontend/src/pages/Chat.tsx
@@ -0,0 +1,167 @@
+import { FormEvent, useEffect, useRef, useState } from "react";
+import ReactMarkdown from "react-markdown";
+import { api, ChatMessage, ChatSession } from "../api/client";
+import "./Chat.css";
+
+export default function Chat() {
+ const [sessions, setSessions] = useState([]);
+ const [activeId, setActiveId] = useState(null);
+ const [messages, setMessages] = useState([]);
+ const [input, setInput] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [streaming, setStreaming] = useState("");
+ const bottomRef = useRef(null);
+
+ const loadSessions = async () => {
+ const data = await api.listSessions();
+ setSessions(data);
+ if (!activeId && data.length > 0) {
+ setActiveId(data[0].id);
+ }
+ };
+
+ const loadMessages = async (sessionId: number) => {
+ const data = await api.getSession(sessionId);
+ setMessages(data.messages);
+ };
+
+ useEffect(() => {
+ loadSessions().catch(console.error);
+ }, []);
+
+ useEffect(() => {
+ if (activeId) {
+ loadMessages(activeId).catch(console.error);
+ }
+ }, [activeId]);
+
+ useEffect(() => {
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages, streaming]);
+
+ const handleNewChat = async () => {
+ const session = await api.createSession();
+ await loadSessions();
+ setActiveId(session.id);
+ setMessages([]);
+ };
+
+ const handleDelete = async (id: number) => {
+ await api.deleteSession(id);
+ const data = await api.listSessions();
+ setSessions(data);
+ if (activeId === id) {
+ setActiveId(data[0]?.id ?? null);
+ setMessages([]);
+ }
+ };
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ if (!input.trim() || !activeId || loading) return;
+
+ const text = input.trim();
+ setInput("");
+ setLoading(true);
+ setStreaming("");
+
+ const tempUser: ChatMessage = {
+ id: Date.now(),
+ role: "user",
+ content: text,
+ created_at: new Date().toISOString(),
+ };
+ setMessages((prev) => [...prev, tempUser]);
+
+ try {
+ for await (const chunk of api.sendMessage(activeId, text)) {
+ if (chunk.event === "token") {
+ setStreaming((prev) => prev + chunk.data.content);
+ }
+ if (chunk.event === "done") {
+ await loadMessages(activeId);
+ await loadSessions();
+ setStreaming("");
+ }
+ if (chunk.event === "error") {
+ throw new Error(chunk.data.message);
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ setStreaming("");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ + Новый чат
+
+
+ {sessions.map((session) => (
+
+ setActiveId(session.id)}>{session.title}
+ handleDelete(session.id)}>
+ ×
+
+
+ ))}
+
+
+
+
+ {!activeId ? (
+ Создайте новый чат, чтобы начать
+ ) : (
+ <>
+
+ {messages.map((msg) => (
+
+
{msg.role}
+
+ {msg.role === "assistant" ? (
+ {msg.content}
+ ) : (
+ msg.content
+ )}
+
+
+ ))}
+ {streaming && (
+
+
assistant
+
+ {streaming}
+
+
+ )}
+
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/frontend/src/pages/Pomodoro.css b/frontend/src/pages/Pomodoro.css
new file mode 100644
index 0000000..3a6de69
--- /dev/null
+++ b/frontend/src/pages/Pomodoro.css
@@ -0,0 +1,130 @@
+.pomodoro-page {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+ padding: 1.5rem;
+ max-width: 1100px;
+ margin: 0 auto;
+}
+
+.timer-card,
+.history-card {
+ background: #151922;
+ border: 1px solid #2a2f3a;
+ border-radius: 16px;
+ padding: 1.5rem;
+}
+
+.timer-ring {
+ width: 220px;
+ height: 220px;
+ border-radius: 50%;
+ margin: 0 auto 1.5rem;
+ display: grid;
+ place-items: center;
+}
+
+.timer-inner {
+ width: 180px;
+ height: 180px;
+ border-radius: 50%;
+ background: #0f1115;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.timer-value {
+ font-size: 2.4rem;
+ font-weight: 700;
+}
+
+.timer-status {
+ color: #8b95a5;
+ text-transform: uppercase;
+ font-size: 0.8rem;
+}
+
+.task-note {
+ text-align: center;
+ color: #c5ccd6;
+}
+
+.timer-form,
+.timer-controls,
+.stop-form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.timer-form label,
+.stop-form label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ color: #a8b0bd;
+}
+
+.timer-form input,
+.stop-form input {
+ border-radius: 8px;
+ border: 1px solid #2f3748;
+ background: #0f1115;
+ color: inherit;
+ padding: 0.6rem 0.8rem;
+}
+
+.timer-controls button,
+.primary-btn {
+ background: #4f7cff;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ padding: 0.65rem 1rem;
+}
+
+.error {
+ color: #ff7b7b;
+ margin-top: 0.75rem;
+}
+
+.history-card h2 {
+ margin-top: 0;
+}
+
+.history-card ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.9rem;
+}
+
+.history-card li {
+ padding-bottom: 0.9rem;
+ border-bottom: 1px solid #232936;
+}
+
+.history-title {
+ font-weight: 600;
+}
+
+.history-meta {
+ color: #8b95a5;
+ font-size: 0.85rem;
+ margin-top: 0.2rem;
+}
+
+.history-result {
+ margin-top: 0.35rem;
+ color: #c5ccd6;
+}
+
+@media (max-width: 900px) {
+ .pomodoro-page {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/frontend/src/pages/Pomodoro.tsx b/frontend/src/pages/Pomodoro.tsx
new file mode 100644
index 0000000..c114eee
--- /dev/null
+++ b/frontend/src/pages/Pomodoro.tsx
@@ -0,0 +1,167 @@
+import { FormEvent, useEffect, useState } from "react";
+import { api, PomodoroHistoryItem, PomodoroStatus } from "../api/client";
+import "./Pomodoro.css";
+
+function formatTime(seconds: number): string {
+ const m = Math.floor(seconds / 60);
+ const s = seconds % 60;
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
+}
+
+export default function Pomodoro() {
+ const [status, setStatus] = useState(null);
+ const [history, setHistory] = useState([]);
+ const [duration, setDuration] = useState(25);
+ const [taskNote, setTaskNote] = useState("");
+ const [result, setResult] = useState("");
+ const [completed, setCompleted] = useState(false);
+ const [error, setError] = useState("");
+
+ const refresh = async () => {
+ const [current, past] = await Promise.all([api.pomodoroStatus(), api.pomodoroHistory()]);
+ setStatus(current);
+ setHistory(past);
+ };
+
+ useEffect(() => {
+ refresh().catch(console.error);
+ const timer = setInterval(() => {
+ api.pomodoroStatus().then(setStatus).catch(console.error);
+ }, 1000);
+ return () => clearInterval(timer);
+ }, []);
+
+ const handleStart = async (e: FormEvent) => {
+ e.preventDefault();
+ setError("");
+ try {
+ const data = await api.pomodoroStart(duration, taskNote);
+ setStatus(data);
+ setResult("");
+ setCompleted(false);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Ошибка запуска");
+ }
+ };
+
+ const handlePause = async () => {
+ setError("");
+ try {
+ setStatus(await api.pomodoroPause());
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Ошибка");
+ }
+ };
+
+ const handleResume = async () => {
+ setError("");
+ try {
+ setStatus(await api.pomodoroResume());
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Ошибка");
+ }
+ };
+
+ const handleStop = async () => {
+ setError("");
+ try {
+ await api.pomodoroStop(result, completed);
+ await refresh();
+ setTaskNote("");
+ setResult("");
+ setCompleted(false);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Ошибка");
+ }
+ };
+
+ const isActive = status?.status === "running" || status?.status === "paused";
+ const displaySeconds = isActive ? (status?.remaining_seconds ?? 0) : duration * 60;
+ const progress = status
+ ? ((status.duration_min * 60 - status.remaining_seconds) / (status.duration_min * 60)) * 100
+ : 0;
+
+ return (
+
+
+
+
+
{formatTime(displaySeconds)}
+
{status?.status ?? "idle"}
+
+
+
+ {status?.task_note && Задача: {status.task_note}
}
+
+ {!isActive ? (
+
+
+ Минут
+ setDuration(Number(e.target.value))}
+ />
+
+
+ Над чем работаем
+ setTaskNote(e.target.value)}
+ placeholder="Опишите задачу"
+ />
+
+
+ Старт
+
+
+ ) : (
+
+ )}
+
+ {error && {error}
}
+
+
+
+
+ );
+}
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..29c29a1
--- /dev/null
+++ b/frontend/src/vite-env.d.ts
@@ -0,0 +1,9 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_API_URL: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..39a405b
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..7366cef
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler"
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo
new file mode 100644
index 0000000..b3a6036
--- /dev/null
+++ b/frontend/tsconfig.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/pages/chat.tsx","./src/pages/pomodoro.tsx"],"version":"5.9.3"}
\ No newline at end of file
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..c14d423
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 5173,
+ proxy: {
+ "/api": {
+ target: "http://localhost:8080",
+ changeOrigin: true,
+ },
+ },
+ },
+});
diff --git a/old_task.txt b/old_task.txt
new file mode 100644
index 0000000..9f87fbb
--- /dev/null
+++ b/old_task.txt
@@ -0,0 +1,525 @@
+grigo@grigosserver:~/to_services$ ls
+3x ai-tools funkwhale jenkins kokoro minecraft netdata openclaw-data qbtorrent spotify.cred taiga xtts
+LoraTester aiChatBot gitea jitsi lidarr navidrome octo-fiesta openclaw-gateway server spotizerr taiga-manual-broken
+acore-docker fishtts jellyfin juice metube neko open-meteo project-agent spotdown stable-10888.zip voice-server
+grigo@grigosserver:~/to_services$ ls ai-tools/
+taiga-tasker
+grigo@grigosserver:~/to_services$ ls ai-tools/taiga-tasker/
+create_taiga_task.py .env .venv/
+grigo@grigosserver:~/to_services$ ls ai-tools/taiga-tasker/create_taiga_task.py
+ai-tools/taiga-tasker/create_taiga_task.py
+grigo@grigosserver:~/to_services$ cat ai-tools/taiga-tasker/create_taiga_task.py
+#!/usr/bin/env python3
+import json
+import os
+import re
+import sys
+from typing import Any
+from pathlib import Path
+import requests
+from dotenv import load_dotenv
+
+SCRIPT_DIR = Path(__file__).resolve().parent
+load_dotenv(SCRIPT_DIR / ".env")
+
+TAIGA_BASE_URL = os.getenv("TAIGA_BASE_URL", "http://127.0.0.1:9000").rstrip("/")
+TAIGA_USERNAME = os.getenv("TAIGA_USERNAME")
+TAIGA_PASSWORD = os.getenv("TAIGA_PASSWORD")
+TAIGA_DEFAULT_PROJECT_ID = os.getenv("TAIGA_DEFAULT_PROJECT_ID")
+
+AIMTR_BASE_URL = os.getenv("AIMTR_BASE_URL", "https://aimtr.wellflow.dev/v1").rstrip("/")
+AIMTR_API_KEY = os.getenv("AIMTR_API_KEY")
+AIMTR_MODEL = os.getenv("AIMTR_MODEL", "claude-haiku-4.5")
+
+
+def require_env() -> None:
+ missing = []
+ for key, value in {
+ "TAIGA_USERNAME": TAIGA_USERNAME,
+ "TAIGA_PASSWORD": TAIGA_PASSWORD,
+ "TAIGA_DEFAULT_PROJECT_ID": TAIGA_DEFAULT_PROJECT_ID,
+ "AIMTR_API_KEY": AIMTR_API_KEY,
+ }.items():
+ if not value:
+ missing.append(key)
+
+ if missing:
+ raise SystemExit(f"Missing env vars in .env: {', '.join(missing)}")
+
+
+def strip_markdown_json(text: str) -> str:
+ text = text.strip()
+
+ fenced = re.search(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL | re.IGNORECASE)
+ if fenced:
+ return fenced.group(1).strip()
+
+ return text
+
+
+def aimtr_make_task(raw_text: str) -> dict[str, Any]:
+ system_prompt = """
+Ты технический ассистент для Taiga.
+Преобразуй сырое описание задачи в строгий JSON.
+Отвечай только JSON, без markdown и пояснений.
+
+Схема:
+{
+ "title": "короткое название задачи",
+ "description": "понятное описание задачи",
+ "type": "Story",
+ "priority": "low|normal|high",
+ "tags": ["tag1", "tag2"],
+ "acceptance_criteria": [
+ "критерий приемки 1",
+ "критерий приемки 2"
+ ],
+ "children": [
+ {
+ "title": "название подзадачи",
+ "description": "описание подзадачи",
+ "type": "Task",
+ "priority": "low|normal|high"
+ }
+ ],
+ "questions": [
+ "уточняющий вопрос, если данных не хватает"
+ ]
+}
+
+Правила:
+- Пиши на русском.
+- Не придумывай бизнес-ограничения, которых нет в запросе.
+- Если данных мало, добавь вопросы в questions.
+- Acceptance criteria должны быть проверяемыми.
+- children должны быть техническими подзадачами.
+""".strip()
+
+ response = requests.post(
+ f"{AIMTR_BASE_URL}/chat/completions",
+ headers={
+ "Authorization": f"Bearer {AIMTR_API_KEY}",
+ "Content-Type": "application/json",
+ },
+ json={
+ "model": AIMTR_MODEL,
+ "messages": [
+ {"role": "system", "content": system_prompt},
+ {"role": "user", "content": raw_text},
+ ],
+ "temperature": 0.2,
+ },
+ timeout=60,
+ )
+ response.raise_for_status()
+
+ content = response.json()["choices"][0]["message"]["content"]
+ clean = strip_markdown_json(content)
+
+ try:
+ return json.loads(clean)
+ except json.JSONDecodeError as exc:
+ print("LLM returned invalid JSON:")
+ print(content)
+ raise SystemExit(exc)
+
+
+def taiga_auth() -> str:
+ response = requests.post(
+ f"{TAIGA_BASE_URL}/api/v1/auth",
+ json={
+ "type": "normal",
+ "username": TAIGA_USERNAME,
+ "password": TAIGA_PASSWORD,
+ },
+ timeout=15,
+ )
+ response.raise_for_status()
+ return response.json()["auth_token"]
+
+
+def taiga_headers(token: str) -> dict[str, str]:
+ return {
+ "Authorization": f"Bearer {token}",
+ "Content-Type": "application/json",
+ }
+
+
+def format_story_description(task: dict[str, Any], raw_text: str) -> str:
+ lines = []
+
+ lines.append(task.get("description") or raw_text)
+
+ acceptance = task.get("acceptance_criteria") or []
+ if acceptance:
+ lines.append("")
+ lines.append("## Acceptance criteria")
+ for item in acceptance:
+ lines.append(f"- {item}")
+
+ questions = task.get("questions") or []
+ if questions:
+ lines.append("")
+ lines.append("## Вопросы / уточнения")
+ for item in questions:
+ lines.append(f"- {item}")
+
+ tags = task.get("tags") or []
+ if tags:
+ lines.append("")
+ lines.append("## Теги")
+ lines.append(", ".join(tags))
+
+ lines.append("")
+ lines.append("## Исходное описание")
+ lines.append(raw_text)
+
+ return "\n".join(lines).strip()
+
+
+def create_userstory(token: str, project_id: int, task: dict[str, Any], raw_text: str) -> dict[str, Any]:
+ subject = (task.get("title") or raw_text).strip()[:500]
+ description = format_story_description(task, raw_text)
+
+ response = requests.post(
+ f"{TAIGA_BASE_URL}/api/v1/userstories",
+ headers=taiga_headers(token),
+ json={
+ "project": project_id,
+ "subject": subject,
+ "description": description,
+ },
+ timeout=15,
+ )
+ response.raise_for_status()
+ return response.json()
+
+
+def create_subtask(token: str, project_id: int, userstory_id: int, child: dict[str, Any]) -> dict[str, Any]:
+ subject = (child.get("title") or "Подзадача").strip()[:500]
+ description = (child.get("description") or "").strip()
+
+ response = requests.post(
+ f"{TAIGA_BASE_URL}/api/v1/tasks",
+ headers=taiga_headers(token),
+ json={
+ "project": project_id,
+ "user_story": userstory_id,
+ "subject": subject,
+ "description": description,
+ },
+ timeout=15,
+ )
+ response.raise_for_status()
+ return response.json()
+
+
+def main() -> None:
+ require_env()
+
+ if len(sys.argv) < 2:
+ raise SystemExit('Usage: ./create_taiga_task.py "Описание задачи" [PROJECT_ID]')
+
+ raw_text = sys.argv[1].strip()
+ project_id = int(sys.argv[2]) if len(sys.argv) > 2 else int(TAIGA_DEFAULT_PROJECT_ID)
+
+ print("Generating structured task with AIMTR...")
+ structured = aimtr_make_task(raw_text)
+
+ print("Authenticating in Taiga...")
+ token = taiga_auth()
+
+ print("Creating user story...")
+ story = create_userstory(token, project_id, structured, raw_text)
+
+ created_tasks = []
+ for child in structured.get("children", []):
+ if isinstance(child, dict):
+ created_tasks.append(create_subtask(token, project_id, story["id"], child))
+
+ print("")
+ print(f"Created user story: #{story['ref']} / id={story['id']}")
+ print(f"Subject: {story['subject']}")
+ print(f"Subtasks created: {len(created_tasks)}")
+
+ for task in created_tasks:
+ print(f"- #{task.get('ref')} {task.get('subject')}")
+
+
+if __name__ == "__main__":
+ main()
+grigo@grigosserver:~/to_services$ ls open
+openclaw-data/ openclaw-gateway/ open-meteo/
+grigo@grigosserver:~/to_services$ ls openclaw-data/
+auth/ config/ workspace/
+grigo@grigosserver:~/to_services$ ls openclaw-data/workspace/
+advanced_integration.c final-checklist.sh main_full.c PINOUT_WIRING.md SOUL.md STEPPER_WIRING.md
+AGENTS.md fix-acore-docker.sh memory/ PROJECT_SUMMARY.sh START_HERE.md test_stepper.c
+auto-setup.sh fix-docker-compose.sh MEMORY.md QUICK_REFERENCE.md stepper_advanced.c TOOLS.md
+catalog_images.py .git/ motor_advanced.py quick-start.sh stepper_examples.py TUNING_GUIDE.md
+CMakeLists.txt HEARTBEAT.md motor_integration.py README_COMPLETE.md stepper_motor.c USER.md
+C_SDK_GUIDE.md IDENTITY.md motor_web_server.py rk3566-yc-p6602.dts stepper_motor_control.py verify-installation.sh
+diagnostics.c install-dependencies.sh .openclaw/ setup_stepper_project.sh stepper_motor.h viewer.html
+FILES_MANIFEST.md main_basic.c organize_stepper_files.sh skills/ STEPPER_QUICK_START.md viewer_server.py
+grigo@grigosserver:~/to_services$ ls openclaw-data/workspace/skills/project_agent/SKILL.md
+openclaw-data/workspace/skills/project_agent/SKILL.md
+grigo@grigosserver:~/to_services$ cat openclaw-data/workspace/skills/project_agent/SKILL.md
+---
+name: project_agent
+description: Manage project-agent projects through API, add/register/update/list/sync projects, create Taiga tasks from natural language, analyze repository code, and choose the next Pomodoro action. Use this instead of MEMORY.md when the user asks to add, register, update, sync, list, or show projects.
+---
+
+# Project Agent
+
+Use this skill when the user asks to:
+- add, register, update, list, show, or sync projects in project-agent;
+- create a development task in Taiga;
+- formalize a feature request, bug, fix, or idea;
+- decompose work into subtasks;
+- analyze repository code before creating a task;
+- choose what to work on next;
+- pick the best task for a Pomodoro;
+- report progress after a Pomodoro.
+
+The project-agent service is available at:
+
+http://project-agent:8787
+
+## Available projects
+
+Project list is dynamic. To see current projects, call:
+
+ curl -sS http://project-agent:8787/projects
+
+Known projects may include:
+- AISHub
+- AIsMas-Web-Service
+- AndroidAisMap
+- PrivateTest
+- Testing
+- ClawSetUp
+
+Default project: AISHub.
+
+## Manage project-agent projects
+
+Use this section when the user asks to:
+- add a project;
+- register a project;
+- create a project in project-agent;
+- update a project;
+- sync a project;
+- list projects;
+- show project config.
+
+Important:
+- Do not write project definitions to MEMORY.md.
+- Do not store project definitions in notes, memory, markdown files, or local summaries.
+- Project definitions must be stored in project-agent via API.
+- Do not say a project was added unless the POST /projects API returned "ok": true.
+- If the user asks to add a project without sync, do not call /sync.
+- Only call /projects/PROJECT_NAME/sync if the user explicitly says the Gitea repository already exists or asks to sync.
+- If required fields are missing, ask only for the missing fields.
+
+Required fields for adding a project:
+- project name;
+- kind: home or work;
+- Taiga project id;
+- Gitea repo in OWNER/REPO format;
+- default branch. Use main if not specified.
+
+To list projects, call:
+
+ curl -sS http://project-agent:8787/projects
+
+To show one project, call:
+
+ curl -sS http://project-agent:8787/projects/PROJECT_NAME
+
+To add a project, call exactly:
+
+ curl -sS -X POST http://project-agent:8787/projects \
+ -H "Content-Type: application/json" \
+ -d '{"name":"PROJECT_NAME","kind":"home","ai_tags":["home"],"taiga_project_id":9,"taiga_slug":"PROJECT_SLUG","gitea_repo":"OWNER/REPO","repo_url":"ssh://git@host.docker.internal:222/OWNER/REPO.git","repo_path":"/repos/PROJECT_NAME","default_branch":"main"}'
+
+To update a project, call exactly:
+
+ curl -sS -X PUT http://project-agent:8787/projects/PROJECT_NAME \
+ -H "Content-Type: application/json" \
+ -d '{"name":"PROJECT_NAME","kind":"home","ai_tags":["home"],"taiga_project_id":9,"taiga_slug":"PROJECT_SLUG","gitea_repo":"OWNER/REPO","repo_url":"ssh://git@host.docker.internal:222/OWNER/REPO.git","repo_path":"/repos/PROJECT_NAME","default_branch":"main"}'
+
+To delete a project, call only when the user explicitly asks to delete or remove it:
+
+ curl -sS -X DELETE http://project-agent:8787/projects/PROJECT_NAME
+
+To sync a project only when requested, call:
+
+ curl -sS -X POST http://project-agent:8787/projects/PROJECT_NAME/sync
+
+After adding or updating a project:
+- call GET /projects/PROJECT_NAME;
+- show the returned config;
+- mention whether sync was skipped or executed;
+- if sync was skipped, say that the project is registered but the repository was not cloned.
+
+Expected response format for project add or update:
+
+Проект сохранён в project-agent:
+- Name: PROJECT_NAME
+- Kind: home/work
+- Taiga ID: ID
+- Gitea repo: OWNER/REPO
+- Repo path: /repos/PROJECT_NAME
+- Default branch: main
+- Sync: skipped/executed
+
+Do not mention MEMORY.md.
+
+## Create code-aware Taiga task
+
+When the user asks to create, formalize, decompose, or register a Taiga task with repository/code context, call exactly this command:
+
+ curl -sS -X POST http://project-agent:8787/tasks/from-code \
+ -H "Content-Type: application/json" \
+ -d '{"project":"PROJECT_NAME","text":"USER_TASK_TEXT"}'
+
+Use this for phrases like:
+- "создай задачу";
+- "оформи хотелку";
+- "заведи bug";
+- "разложи на подзадачи";
+- "создай задачу с учетом кода";
+- "вот список ошибок и хотелок";
+- "добавь это в Taiga".
+
+Replace:
+- PROJECT_NAME with one of the available projects. Use AISHub if the project is not specified.
+- USER_TASK_TEXT with the user's full task description.
+
+After the call, always include these fields from the JSON response:
+- Taiga story: story.ref, story.id, story.subject
+- Gitea issue: gitea_issue.url
+- Suggested branch: gitea_issue.branch
+- Labels: gitea_issue.labels
+- Created subtasks: subtasks[].ref and subtasks[].subject
+- Useful code notes: structured.code_notes
+- Blocking questions only: structured.questions
+
+Do not omit Gitea issue, branch, or labels if they exist in the JSON response.
+
+Mandatory response format for /tasks/from-code:
+
+Создано:
+- Taiga: #{{story.ref}} — {{story.subject}}
+- Gitea: {{gitea_issue.url}}
+- Рекомендуемая ветка: {{gitea_issue.branch}}
+- Labels: {{gitea_issue.labels}}
+
+Подзадачи:
+{{for each item in subtasks}}
+- #{{ref}} — {{subject}}
+
+Заметки по коду:
+{{for each item in structured.code_notes}}
+- {{item}}
+
+Вопросы/блокеры:
+{{only include structured.questions that block implementation}}
+
+Rules:
+- Use the exact URL from gitea_issue.url.
+- Use the exact branch from gitea_issue.branch.
+- Use the exact labels from gitea_issue.labels.
+- Do not say "branch created" unless the JSON explicitly contains "branch_created": true.
+- Do not claim a subtask count unless you count the subtasks array exactly.
+- Do not rewrite the response into a generic project-management summary.
+
+## Pick next Pomodoro action
+
+When the user asks what to work on next, what is the current priority, or what task to do during a Pomodoro, call exactly this command:
+
+ curl -sS -X POST http://project-agent:8787/next-action \
+ -H "Content-Type: application/json" \
+ -d '{"project":"PROJECT_NAME","minutes":25,"energy":"normal","notes":"USER_CONTEXT"}'
+
+Use this for phrases like:
+- "Я завожу помидор, что делать?";
+- "Что сейчас в приоритете?";
+- "Какую задачу взять на 25 минут?";
+- "Что делать дальше по AISHub?";
+- "Выбери следующую задачу";
+- "Помоги выбрать задачу на сейчас".
+
+Replace:
+- PROJECT_NAME with one of the available projects. Use AISHub if the project is not specified.
+- minutes with the user's Pomodoro duration. Use 25 if not specified.
+- energy with low, normal, or high. Use normal if not specified.
+- USER_CONTEXT with the user's current context, constraints, and notes.
+
+Important:
+- Return exactly ONE recommended task.
+- Start the answer with the task ref and title.
+- Do not offer alternative tasks unless the selected task is blocked.
+- Do not say "or you can do X".
+- Do not ask "what do you choose?" after recommending a Pomodoro task.
+- Give a concrete execution plan for the requested timebox.
+- Give a clear definition of done.
+- Mention open questions only if they block the selected task.
+- Use only POST /next-action.
+- Do not call /tasks.
+- Do not call /next-task.
+
+After the call, format the response like this:
+
+Бери #REF — "TITLE".
+
+Почему сейчас:
+...
+
+План на 25 минут:
+1. ...
+2. ...
+3. ...
+
+Definition of done:
+- ...
+- ...
+
+Блокер/вопрос:
+...
+
+If there is no blocker, omit the blocker section.
+
+## Finish Pomodoro / report progress
+
+When the user reports what they did after a Pomodoro, call exactly this command:
+
+ curl -sS -X POST http://project-agent:8787/pomodoro/finish \
+ -H "Content-Type: application/json" \
+ -d '{"project":"PROJECT_NAME","task_ref":TASK_REF,"minutes":25,"result":"USER_REPORT","done":false}'
+
+Use this for phrases like:
+- "я закончил помидор";
+- "отчет по задаче";
+- "сделал X, не успел Y";
+- "закрой задачу";
+- "готово";
+- "задача #17 сделана".
+
+Rules:
+- If the user says the task is finished, set done=true.
+- If the user only reports partial progress, set done=false.
+- If task ref is missing, ask for the task number.
+- After the call, summarize what was recorded in Taiga.
+- Then return the next recommended Pomodoro action from the response.
+
+## Safety
+
+- Use only the project-agent API.
+- Do not access Taiga, Gitea, Jenkins, or repositories directly.
+- Do not ask the user for Taiga URL, token, username, or password.
+- Do not print secrets, .env contents, tokens, passwords, SSH keys, or Authorization headers.
+- Do not delete, push, merge, deploy, or modify repositories.
+- Do not run arbitrary shell commands unrelated to project-agent.
+- Do not write project definitions to MEMORY.md.
+grigo@grigosserver:~/to_services$
\ No newline at end of file