From 9c38d4013e2f16f760de69004f238c7164a9a768 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Sun, 30 Nov 2025 16:30:48 +0000 Subject: [PATCH 01/21] moved memory and transcription to services --- .gitmodules | 3 + .../src/advanced_omi_backend/app_factory.py | 2 +- .../src/advanced_omi_backend/chat_service.py | 2 +- .../src/advanced_omi_backend/config.py | 6 +- .../controllers/memory_controller.py | 2 +- .../controllers/system_controller.py | 4 +- .../controllers/user_controller.py | 2 +- .../memory/providers/__init__.py | 41 --- .../models/conversation.py | 1 + .../routers/modules/health_routes.py | 2 +- .../services/audio_stream/producer.py | 2 +- .../{ => services}/memory/README.md | 0 .../{ => services}/memory/__init__.py | 10 +- .../{ => services}/memory/base.py | 0 .../{ => services}/memory/config.py | 19 ++ .../{ => services}/memory/prompts.py | 0 .../services/memory/providers/__init__.py | 30 ++ .../memory/providers}/compat_service.py | 8 +- .../memory/providers/friend_lite.py} | 14 +- .../memory/providers/llm_providers.py | 0 .../memory/providers/mcp_client.py | 0 .../services/memory/providers/mycelia.py | 277 ++++++++++++++++++ .../memory/providers/openmemory_mcp.py} | 0 .../memory/providers/vector_stores.py | 0 .../{ => services}/memory/service_factory.py | 24 +- .../memory/update_memory_utils.py | 0 .../{ => services}/memory/utils.py | 0 .../services/transcription/__init__.py | 2 +- .../transcription/base.py} | 0 .../services/transcription/deepgram.py | 2 +- .../services/transcription/parakeet.py | 2 +- .../utils/conversation_utils.py | 15 +- .../workers/memory_jobs.py | 4 +- .../workers/transcription_jobs.py | 69 +++++ extras/mycelia | 1 + 35 files changed, 462 insertions(+), 82 deletions(-) create mode 100644 .gitmodules delete mode 100644 backends/advanced/src/advanced_omi_backend/memory/providers/__init__.py rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/README.md (100%) rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/__init__.py (92%) rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/base.py (100%) rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/config.py (95%) rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/prompts.py (100%) create mode 100644 backends/advanced/src/advanced_omi_backend/services/memory/providers/__init__.py rename backends/advanced/src/advanced_omi_backend/{memory => services/memory/providers}/compat_service.py (98%) rename backends/advanced/src/advanced_omi_backend/{memory/memory_service.py => services/memory/providers/friend_lite.py} (99%) rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/providers/llm_providers.py (100%) rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/providers/mcp_client.py (100%) create mode 100644 backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py rename backends/advanced/src/advanced_omi_backend/{memory/providers/openmemory_mcp_service.py => services/memory/providers/openmemory_mcp.py} (100%) rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/providers/vector_stores.py (100%) rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/service_factory.py (89%) rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/update_memory_utils.py (100%) rename backends/advanced/src/advanced_omi_backend/{ => services}/memory/utils.py (100%) rename backends/advanced/src/advanced_omi_backend/{models/transcription.py => services/transcription/base.py} (100%) create mode 160000 extras/mycelia diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..ffffaa52 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "extras/mycelia"] + path = extras/mycelia + url = https://github.com/mycelia-tech/mycelia diff --git a/backends/advanced/src/advanced_omi_backend/app_factory.py b/backends/advanced/src/advanced_omi_backend/app_factory.py index 52a48093..8aa0c97a 100644 --- a/backends/advanced/src/advanced_omi_backend/app_factory.py +++ b/backends/advanced/src/advanced_omi_backend/app_factory.py @@ -30,7 +30,7 @@ register_client_to_user, ) from advanced_omi_backend.client_manager import get_client_manager -from advanced_omi_backend.memory import get_memory_service, shutdown_memory_service +from advanced_omi_backend.services.memory import get_memory_service, shutdown_memory_service from advanced_omi_backend.middleware.app_middleware import setup_middleware from advanced_omi_backend.routers.api_router import router as api_router from advanced_omi_backend.routers.modules.health_routes import router as health_router diff --git a/backends/advanced/src/advanced_omi_backend/chat_service.py b/backends/advanced/src/advanced_omi_backend/chat_service.py index 812f8af0..4ec5ecff 100644 --- a/backends/advanced/src/advanced_omi_backend/chat_service.py +++ b/backends/advanced/src/advanced_omi_backend/chat_service.py @@ -22,7 +22,7 @@ from advanced_omi_backend.database import get_database from advanced_omi_backend.llm_client import get_llm_client -from advanced_omi_backend.memory import get_memory_service +from advanced_omi_backend.services.memory import get_memory_service from advanced_omi_backend.users import User logger = logging.getLogger(__name__) diff --git a/backends/advanced/src/advanced_omi_backend/config.py b/backends/advanced/src/advanced_omi_backend/config.py index ceebcad0..f2168e6d 100644 --- a/backends/advanced/src/advanced_omi_backend/config.py +++ b/backends/advanced/src/advanced_omi_backend/config.py @@ -30,9 +30,9 @@ # Default speech detection settings DEFAULT_SPEECH_DETECTION_SETTINGS = { - "min_words": 5, # Minimum words to create conversation - "min_confidence": 0.5, # Word confidence threshold (unified) - "min_duration": 2.0, # Minimum speech duration (seconds) + "min_words": 10, # Minimum words to create conversation (increased from 5) + "min_confidence": 0.7, # Word confidence threshold (increased from 0.5) + "min_duration": 10.0, # Minimum speech duration in seconds (increased from 2.0) } # Default conversation stop settings diff --git a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py index e5f576c2..f6ca8387 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py @@ -8,7 +8,7 @@ from fastapi.responses import JSONResponse -from advanced_omi_backend.memory import get_memory_service +from advanced_omi_backend.services.memory import get_memory_service from advanced_omi_backend.users import User logger = logging.getLogger(__name__) diff --git a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py index 5bc0b35d..a2afadbc 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py @@ -442,8 +442,8 @@ async def reload_memory_config(): async def delete_all_user_memories(user: User): """Delete all memories for the current user.""" try: - from advanced_omi_backend.memory import get_memory_service - + from advanced_omi_backend.services.memory import get_memory_service + memory_service = get_memory_service() # Delete all memories for the user diff --git a/backends/advanced/src/advanced_omi_backend/controllers/user_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/user_controller.py index ba7dd753..a1b9c140 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/user_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/user_controller.py @@ -16,7 +16,7 @@ ) from advanced_omi_backend.client_manager import get_user_clients_all from advanced_omi_backend.database import db, users_col -from advanced_omi_backend.memory import get_memory_service +from advanced_omi_backend.services.memory import get_memory_service from advanced_omi_backend.models.conversation import Conversation from advanced_omi_backend.users import User, UserCreate, UserUpdate diff --git a/backends/advanced/src/advanced_omi_backend/memory/providers/__init__.py b/backends/advanced/src/advanced_omi_backend/memory/providers/__init__.py deleted file mode 100644 index 59ded58e..00000000 --- a/backends/advanced/src/advanced_omi_backend/memory/providers/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Memory service providers package. - -This package contains implementations of LLM providers, vector stores, -and complete memory service implementations for the memory service architecture. -""" - -from ..base import LLMProviderBase, VectorStoreBase, MemoryEntry -from .llm_providers import OpenAIProvider -from .vector_stores import QdrantVectorStore - -# Import complete memory service implementations -try: - from .openmemory_mcp_service import OpenMemoryMCPService -except ImportError: - OpenMemoryMCPService = None - -try: - from .mcp_client import MCPClient, MCPError -except ImportError: - MCPClient = None - MCPError = None - -__all__ = [ - # Base classes - "LLMProviderBase", - "VectorStoreBase", - "MemoryEntry", - - # LLM providers - "OpenAIProvider", - - # Vector stores - "QdrantVectorStore", - - # Complete memory service implementations - "OpenMemoryMCPService", - - # MCP client components - "MCPClient", - "MCPError", -] \ No newline at end of file diff --git a/backends/advanced/src/advanced_omi_backend/models/conversation.py b/backends/advanced/src/advanced_omi_backend/models/conversation.py index 7caf8a55..55c31244 100644 --- a/backends/advanced/src/advanced_omi_backend/models/conversation.py +++ b/backends/advanced/src/advanced_omi_backend/models/conversation.py @@ -30,6 +30,7 @@ class MemoryProvider(str, Enum): """Supported memory providers.""" FRIEND_LITE = "friend_lite" OPENMEMORY_MCP = "openmemory_mcp" + MYCELIA = "mycelia" class ConversationStatus(str, Enum): """Conversation processing status.""" diff --git a/backends/advanced/src/advanced_omi_backend/routers/modules/health_routes.py b/backends/advanced/src/advanced_omi_backend/routers/modules/health_routes.py index 37913c48..1634bc3d 100644 --- a/backends/advanced/src/advanced_omi_backend/routers/modules/health_routes.py +++ b/backends/advanced/src/advanced_omi_backend/routers/modules/health_routes.py @@ -18,7 +18,7 @@ from advanced_omi_backend.controllers.queue_controller import redis_conn from advanced_omi_backend.client_manager import get_client_manager from advanced_omi_backend.llm_client import async_health_check -from advanced_omi_backend.memory import get_memory_service +from advanced_omi_backend.services.memory import get_memory_service from advanced_omi_backend.services.transcription import get_transcription_provider # Create router diff --git a/backends/advanced/src/advanced_omi_backend/services/audio_stream/producer.py b/backends/advanced/src/advanced_omi_backend/services/audio_stream/producer.py index 95bf25e1..66b0acf7 100644 --- a/backends/advanced/src/advanced_omi_backend/services/audio_stream/producer.py +++ b/backends/advanced/src/advanced_omi_backend/services/audio_stream/producer.py @@ -7,7 +7,7 @@ import redis.asyncio as redis -from advanced_omi_backend.models.transcription import TranscriptionProvider +from advanced_omi_backend.services.transcription.base import TranscriptionProvider logger = logging.getLogger(__name__) diff --git a/backends/advanced/src/advanced_omi_backend/memory/README.md b/backends/advanced/src/advanced_omi_backend/services/memory/README.md similarity index 100% rename from backends/advanced/src/advanced_omi_backend/memory/README.md rename to backends/advanced/src/advanced_omi_backend/services/memory/README.md diff --git a/backends/advanced/src/advanced_omi_backend/memory/__init__.py b/backends/advanced/src/advanced_omi_backend/services/memory/__init__.py similarity index 92% rename from backends/advanced/src/advanced_omi_backend/memory/__init__.py rename to backends/advanced/src/advanced_omi_backend/services/memory/__init__.py index 1fcc786a..42cba194 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/__init__.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/__init__.py @@ -27,7 +27,7 @@ memory_logger.info("๐Ÿ†• Using NEW memory service implementation") try: - from .compat_service import ( + from .providers.compat_service import ( MemoryService, get_memory_service, migrate_from_mem0, @@ -35,7 +35,7 @@ ) # Also import core implementation for direct access - from .memory_service import MemoryService as CoreMemoryService + from .providers.friend_lite import MemoryService as CoreMemoryService test_new_memory_service = None # Will be implemented if needed except ImportError as e: memory_logger.error(f"Failed to import new memory service: {e}") @@ -55,8 +55,10 @@ create_openai_config, create_qdrant_config, ) - from .providers import OpenMemoryMCPService # New complete memory service - from .providers import MCPClient, MCPError, OpenAIProvider, QdrantVectorStore + from .providers.openmemory_mcp import OpenMemoryMCPService # New complete memory service + from .providers.mcp_client import MCPClient, MCPError + from .providers.llm_providers import OpenAIProvider + from .providers.vector_stores import QdrantVectorStore from .service_factory import create_memory_service from .service_factory import get_memory_service as get_core_memory_service from .service_factory import get_service_info as get_core_service_info diff --git a/backends/advanced/src/advanced_omi_backend/memory/base.py b/backends/advanced/src/advanced_omi_backend/services/memory/base.py similarity index 100% rename from backends/advanced/src/advanced_omi_backend/memory/base.py rename to backends/advanced/src/advanced_omi_backend/services/memory/base.py diff --git a/backends/advanced/src/advanced_omi_backend/memory/config.py b/backends/advanced/src/advanced_omi_backend/services/memory/config.py similarity index 95% rename from backends/advanced/src/advanced_omi_backend/memory/config.py rename to backends/advanced/src/advanced_omi_backend/services/memory/config.py index 99e79d38..ae03fcd8 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/config.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/config.py @@ -36,6 +36,7 @@ class MemoryProvider(Enum): """Supported memory service providers.""" FRIEND_LITE = "friend_lite" # Default sophisticated implementation OPENMEMORY_MCP = "openmemory_mcp" # OpenMemory MCP backend + MYCELIA = "mycelia" # Mycelia memory backend @dataclass @@ -48,6 +49,7 @@ class MemoryConfig: vector_store_config: Dict[str, Any] = None embedder_config: Dict[str, Any] = None openmemory_config: Dict[str, Any] = None # Configuration for OpenMemory MCP + mycelia_config: Dict[str, Any] = None # Configuration for Mycelia extraction_prompt: str = None extraction_enabled: bool = True timeout_seconds: int = 1200 @@ -122,6 +124,23 @@ def create_openmemory_config( } +def create_mycelia_config( + api_url: str = "http://localhost:8080", + api_key: str = None, + timeout: int = 30, + **kwargs +) -> Dict[str, Any]: + """Create Mycelia configuration.""" + config = { + "api_url": api_url, + "timeout": timeout, + } + if api_key: + config["api_key"] = api_key + config.update(kwargs) + return config + + def build_memory_config_from_env() -> MemoryConfig: """Build memory configuration from environment variables and YAML config.""" try: diff --git a/backends/advanced/src/advanced_omi_backend/memory/prompts.py b/backends/advanced/src/advanced_omi_backend/services/memory/prompts.py similarity index 100% rename from backends/advanced/src/advanced_omi_backend/memory/prompts.py rename to backends/advanced/src/advanced_omi_backend/services/memory/prompts.py diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/__init__.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/__init__.py new file mode 100644 index 00000000..591fbc2b --- /dev/null +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/__init__.py @@ -0,0 +1,30 @@ +"""Memory service provider implementations. + +This package contains all memory service provider implementations: +- friend_lite: Friend-Lite native implementation with LLM + vector store +- openmemory_mcp: OpenMemory MCP backend integration +- mycelia: Mycelia backend integration +- llm_providers: LLM provider implementations (OpenAI, Ollama) +- vector_stores: Vector store implementations (Qdrant) +- mcp_client: MCP client utilities +- compat_service: Backward compatibility wrapper +""" + +from .friend_lite import MemoryService as FriendLiteMemoryService +from .openmemory_mcp import OpenMemoryMCPService +from .mycelia import MyceliaMemoryService +from .llm_providers import OpenAIProvider +from .vector_stores import QdrantVectorStore +from .mcp_client import MCPClient, MCPError +from .compat_service import MemoryService + +__all__ = [ + "FriendLiteMemoryService", + "OpenMemoryMCPService", + "MyceliaMemoryService", + "OpenAIProvider", + "QdrantVectorStore", + "MCPClient", + "MCPError", + "MemoryService", +] diff --git a/backends/advanced/src/advanced_omi_backend/memory/compat_service.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/compat_service.py similarity index 98% rename from backends/advanced/src/advanced_omi_backend/memory/compat_service.py rename to backends/advanced/src/advanced_omi_backend/services/memory/providers/compat_service.py index 3814f29e..361f8bcd 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/compat_service.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/compat_service.py @@ -10,8 +10,8 @@ import os from typing import Any, Dict, List, Optional, Tuple -from .config import build_memory_config_from_env -from .memory_service import MemoryService as CoreMemoryService +from ..config import build_memory_config_from_env +from .friend_lite import MemoryService as CoreMemoryService memory_logger = logging.getLogger("memory_service") @@ -395,8 +395,8 @@ def get_memory_service() -> MemoryService: global _memory_service if _memory_service is None: # Use the new service factory to create the appropriate service - from .service_factory import get_memory_service as get_core_service - + from ..service_factory import get_memory_service as get_core_service + core_service = get_core_service() # If it's already a compat service, use it directly diff --git a/backends/advanced/src/advanced_omi_backend/memory/memory_service.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py similarity index 99% rename from backends/advanced/src/advanced_omi_backend/memory/memory_service.py rename to backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py index 6460aa25..be91a5f5 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/memory_service.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py @@ -11,15 +11,11 @@ import uuid from typing import Any, List, Optional, Tuple -from .base import MemoryEntry, MemoryServiceBase -from .config import LLMProvider as LLMProviderEnum -from .config import MemoryConfig, VectorStoreProvider -from .providers import ( - LLMProviderBase, - OpenAIProvider, - QdrantVectorStore, - VectorStoreBase, -) +from ..base import LLMProviderBase, MemoryEntry, MemoryServiceBase, VectorStoreBase +from ..config import LLMProvider as LLMProviderEnum +from ..config import MemoryConfig, VectorStoreProvider +from .llm_providers import OpenAIProvider +from .vector_stores import QdrantVectorStore memory_logger = logging.getLogger("memory_service") diff --git a/backends/advanced/src/advanced_omi_backend/memory/providers/llm_providers.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/llm_providers.py similarity index 100% rename from backends/advanced/src/advanced_omi_backend/memory/providers/llm_providers.py rename to backends/advanced/src/advanced_omi_backend/services/memory/providers/llm_providers.py diff --git a/backends/advanced/src/advanced_omi_backend/memory/providers/mcp_client.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py similarity index 100% rename from backends/advanced/src/advanced_omi_backend/memory/providers/mcp_client.py rename to backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py new file mode 100644 index 00000000..ccf30160 --- /dev/null +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py @@ -0,0 +1,277 @@ +"""Mycelia memory service implementation. + +This module provides a concrete implementation of the MemoryServiceBase interface +that uses Mycelia as the backend for all memory operations. +""" + +import logging +from typing import Any, List, Optional, Tuple + +from ..base import MemoryEntry, MemoryServiceBase + +memory_logger = logging.getLogger("memory_service") + + +class MyceliaMemoryService(MemoryServiceBase): + """Memory service implementation using Mycelia backend. + + This class implements the MemoryServiceBase interface by delegating memory + operations to a Mycelia server. + + Args: + api_url: Mycelia API endpoint URL + api_key: Optional API key for authentication + timeout: Request timeout in seconds + **kwargs: Additional configuration parameters + """ + + def __init__( + self, + api_url: str = "http://localhost:8080", + api_key: Optional[str] = None, + timeout: int = 30, + **kwargs + ): + """Initialize Mycelia memory service. + + Args: + api_url: Mycelia API endpoint + api_key: Optional API key for authentication + timeout: Request timeout in seconds + **kwargs: Additional configuration parameters + """ + self.api_url = api_url + self.api_key = api_key + self.timeout = timeout + self.config = kwargs + self._initialized = False + + memory_logger.info(f"๐Ÿ„ Initializing Mycelia memory service at {api_url}") + + async def initialize(self) -> None: + """Initialize Mycelia client and verify connection.""" + try: + # TODO: Initialize your Mycelia client here + # Example: self.client = MyceliaClient(self.api_url, self.api_key) + + # Test connection + if not await self.test_connection(): + raise RuntimeError("Failed to connect to Mycelia service") + + self._initialized = True + memory_logger.info("โœ… Mycelia memory service initialized successfully") + + except Exception as e: + memory_logger.error(f"โŒ Failed to initialize Mycelia service: {e}") + raise RuntimeError(f"Mycelia initialization failed: {e}") + + async def add_memory( + self, + transcript: str, + client_id: str, + source_id: str, + user_id: str, + user_email: str, + allow_update: bool = False, + db_helper: Any = None, + ) -> Tuple[bool, List[str]]: + """Add memories from transcript using Mycelia. + + Args: + transcript: Raw transcript text to extract memories from + client_id: Client identifier + source_id: Unique identifier for the source (audio session, chat session, etc.) + user_id: User identifier + user_email: User email address + allow_update: Whether to allow updating existing memories + db_helper: Optional database helper for tracking relationships + + Returns: + Tuple of (success: bool, created_memory_ids: List[str]) + """ + try: + # TODO: Implement your Mycelia API call to add memories + # Example implementation: + # response = await self.client.add_memories( + # transcript=transcript, + # user_id=user_id, + # metadata={ + # "client_id": client_id, + # "source_id": source_id, + # "user_email": user_email, + # } + # ) + # return (True, response.memory_ids) + + memory_logger.warning("Mycelia add_memory not yet implemented") + return (False, []) + + except Exception as e: + memory_logger.error(f"Failed to add memory via Mycelia: {e}") + return (False, []) + + async def search_memories( + self, query: str, user_id: str, limit: int = 10, score_threshold: float = 0.0 + ) -> List[MemoryEntry]: + """Search memories using Mycelia semantic search. + + Args: + query: Search query text + user_id: User identifier to filter memories + limit: Maximum number of results to return + score_threshold: Minimum similarity score (0.0 = no threshold) + + Returns: + List of matching MemoryEntry objects ordered by relevance + """ + try: + # TODO: Implement Mycelia search + # Example implementation: + # results = await self.client.search( + # query=query, + # user_id=user_id, + # limit=limit, + # threshold=score_threshold + # ) + # return [ + # MemoryEntry( + # id=r.id, + # memory=r.text, + # user_id=user_id, + # metadata=r.metadata, + # score=r.score + # ) + # for r in results + # ] + + memory_logger.warning("Mycelia search_memories not yet implemented") + return [] + + except Exception as e: + memory_logger.error(f"Failed to search memories via Mycelia: {e}") + return [] + + async def get_all_memories( + self, user_id: str, limit: int = 100 + ) -> List[MemoryEntry]: + """Get all memories for a user from Mycelia. + + Args: + user_id: User identifier + limit: Maximum number of memories to return + + Returns: + List of MemoryEntry objects for the user + """ + try: + # TODO: Implement Mycelia get all + # Example implementation: + # results = await self.client.get_all(user_id=user_id, limit=limit) + # return [ + # MemoryEntry( + # id=r.id, + # memory=r.text, + # user_id=user_id, + # metadata=r.metadata + # ) + # for r in results + # ] + + memory_logger.warning("Mycelia get_all_memories not yet implemented") + return [] + + except Exception as e: + memory_logger.error(f"Failed to get memories via Mycelia: {e}") + return [] + + async def count_memories(self, user_id: str) -> Optional[int]: + """Count memories for a user. + + Args: + user_id: User identifier + + Returns: + Total count of memories for the user, or None if not supported + """ + try: + # TODO: Implement if Mycelia supports efficient counting + # Example: + # return await self.client.count(user_id=user_id) + + return None # Not implemented yet + + except Exception as e: + memory_logger.error(f"Failed to count memories via Mycelia: {e}") + return None + + async def delete_memory(self, memory_id: str) -> bool: + """Delete a specific memory from Mycelia. + + Args: + memory_id: Unique identifier of the memory to delete + + Returns: + True if successfully deleted, False otherwise + """ + try: + # TODO: Implement Mycelia delete + # Example: + # success = await self.client.delete(memory_id=memory_id) + # return success + + memory_logger.warning("Mycelia delete_memory not yet implemented") + return False + + except Exception as e: + memory_logger.error(f"Failed to delete memory via Mycelia: {e}") + return False + + async def delete_all_user_memories(self, user_id: str) -> int: + """Delete all memories for a user from Mycelia. + + Args: + user_id: User identifier + + Returns: + Number of memories that were deleted + """ + try: + # TODO: Implement Mycelia bulk delete + # Example: + # count = await self.client.delete_all(user_id=user_id) + # return count + + memory_logger.warning("Mycelia delete_all_user_memories not yet implemented") + return 0 + + except Exception as e: + memory_logger.error(f"Failed to delete user memories via Mycelia: {e}") + return 0 + + async def test_connection(self) -> bool: + """Test connection to Mycelia service. + + Returns: + True if connection is healthy, False otherwise + """ + try: + # TODO: Implement health check + # Example: + # return await self.client.health_check() + + # For now, just check if URL is set + memory_logger.warning("Mycelia test_connection not fully implemented (stub)") + return self.api_url is not None + + except Exception as e: + memory_logger.error(f"Mycelia connection test failed: {e}") + return False + + def shutdown(self) -> None: + """Shutdown Mycelia client and cleanup resources.""" + memory_logger.info("Shutting down Mycelia memory service") + # TODO: Cleanup if needed + # Example: + # if hasattr(self, 'client'): + # self.client.close() + self._initialized = False diff --git a/backends/advanced/src/advanced_omi_backend/memory/providers/openmemory_mcp_service.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py similarity index 100% rename from backends/advanced/src/advanced_omi_backend/memory/providers/openmemory_mcp_service.py rename to backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py diff --git a/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/vector_stores.py similarity index 100% rename from backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py rename to backends/advanced/src/advanced_omi_backend/services/memory/providers/vector_stores.py diff --git a/backends/advanced/src/advanced_omi_backend/memory/service_factory.py b/backends/advanced/src/advanced_omi_backend/services/memory/service_factory.py similarity index 89% rename from backends/advanced/src/advanced_omi_backend/memory/service_factory.py rename to backends/advanced/src/advanced_omi_backend/services/memory/service_factory.py index df2a23c9..a51f4edc 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/service_factory.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/service_factory.py @@ -38,21 +38,33 @@ def create_memory_service(config: MemoryConfig) -> MemoryServiceBase: if config.memory_provider == MemoryProvider.FRIEND_LITE: # Use the sophisticated Friend-Lite implementation - from .memory_service import MemoryService as FriendLiteMemoryService + from .providers.friend_lite import MemoryService as FriendLiteMemoryService return FriendLiteMemoryService(config) - + elif config.memory_provider == MemoryProvider.OPENMEMORY_MCP: # Use OpenMemory MCP implementation try: - from .providers.openmemory_mcp_service import OpenMemoryMCPService + from .providers.openmemory_mcp import OpenMemoryMCPService except ImportError as e: raise RuntimeError(f"OpenMemory MCP service not available: {e}") - + if not config.openmemory_config: raise ValueError("OpenMemory configuration is required for OPENMEMORY_MCP provider") - + return OpenMemoryMCPService(**config.openmemory_config) - + + elif config.memory_provider == MemoryProvider.MYCELIA: + # Use Mycelia implementation + try: + from .providers.mycelia import MyceliaMemoryService + except ImportError as e: + raise RuntimeError(f"Mycelia memory service not available: {e}") + + if not config.mycelia_config: + raise ValueError("Mycelia configuration is required for MYCELIA provider") + + return MyceliaMemoryService(**config.mycelia_config) + else: raise ValueError(f"Unsupported memory provider: {config.memory_provider}") diff --git a/backends/advanced/src/advanced_omi_backend/memory/update_memory_utils.py b/backends/advanced/src/advanced_omi_backend/services/memory/update_memory_utils.py similarity index 100% rename from backends/advanced/src/advanced_omi_backend/memory/update_memory_utils.py rename to backends/advanced/src/advanced_omi_backend/services/memory/update_memory_utils.py diff --git a/backends/advanced/src/advanced_omi_backend/memory/utils.py b/backends/advanced/src/advanced_omi_backend/services/memory/utils.py similarity index 100% rename from backends/advanced/src/advanced_omi_backend/memory/utils.py rename to backends/advanced/src/advanced_omi_backend/services/memory/utils.py diff --git a/backends/advanced/src/advanced_omi_backend/services/transcription/__init__.py b/backends/advanced/src/advanced_omi_backend/services/transcription/__init__.py index 9036aa61..06d5b57f 100644 --- a/backends/advanced/src/advanced_omi_backend/services/transcription/__init__.py +++ b/backends/advanced/src/advanced_omi_backend/services/transcription/__init__.py @@ -10,7 +10,7 @@ import os from typing import Optional -from advanced_omi_backend.models.transcription import BaseTranscriptionProvider +from .base import BaseTranscriptionProvider from advanced_omi_backend.services.transcription.deepgram import ( DeepgramProvider, DeepgramStreamingProvider, diff --git a/backends/advanced/src/advanced_omi_backend/models/transcription.py b/backends/advanced/src/advanced_omi_backend/services/transcription/base.py similarity index 100% rename from backends/advanced/src/advanced_omi_backend/models/transcription.py rename to backends/advanced/src/advanced_omi_backend/services/transcription/base.py diff --git a/backends/advanced/src/advanced_omi_backend/services/transcription/deepgram.py b/backends/advanced/src/advanced_omi_backend/services/transcription/deepgram.py index e9261955..ee7e23fa 100644 --- a/backends/advanced/src/advanced_omi_backend/services/transcription/deepgram.py +++ b/backends/advanced/src/advanced_omi_backend/services/transcription/deepgram.py @@ -13,7 +13,7 @@ import httpx import websockets -from advanced_omi_backend.models.transcription import ( +from .base import ( BatchTranscriptionProvider, StreamingTranscriptionProvider, ) diff --git a/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet.py b/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet.py index 5b11e094..97b5b751 100644 --- a/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet.py +++ b/backends/advanced/src/advanced_omi_backend/services/transcription/parakeet.py @@ -17,7 +17,7 @@ from easy_audio_interfaces.audio_interfaces import AudioChunk from easy_audio_interfaces.filesystem import LocalFileSink -from advanced_omi_backend.models.transcription import ( +from .base import ( BatchTranscriptionProvider, StreamingTranscriptionProvider, ) diff --git a/backends/advanced/src/advanced_omi_backend/utils/conversation_utils.py b/backends/advanced/src/advanced_omi_backend/utils/conversation_utils.py index 416c1fb1..b2cddf4c 100644 --- a/backends/advanced/src/advanced_omi_backend/utils/conversation_utils.py +++ b/backends/advanced/src/advanced_omi_backend/utils/conversation_utils.py @@ -52,8 +52,9 @@ def analyze_speech(transcript_data: dict) -> dict: Analyze transcript for meaningful speech to determine if conversation should be created. Uses configurable thresholds from environment: - - SPEECH_DETECTION_MIN_WORDS (default: 5) - - SPEECH_DETECTION_MIN_CONFIDENCE (default: 0.5) + - SPEECH_DETECTION_MIN_WORDS (default: 10) + - SPEECH_DETECTION_MIN_CONFIDENCE (default: 0.7) + - SPEECH_DETECTION_MIN_DURATION (default: 10.0) Args: transcript_data: Dictionary with: @@ -99,6 +100,16 @@ def analyze_speech(transcript_data: dict) -> dict: speech_end = valid_words[-1].get("end", 0) speech_duration = speech_end - speech_start + # Check minimum duration threshold + min_duration = settings.get("min_duration", 10.0) + if speech_duration < min_duration: + return { + "has_speech": False, + "reason": f"Speech too short ({speech_duration:.1f}s < {min_duration}s)", + "word_count": len(valid_words), + "duration": speech_duration, + } + return { "has_speech": True, "word_count": len(valid_words), diff --git a/backends/advanced/src/advanced_omi_backend/workers/memory_jobs.py b/backends/advanced/src/advanced_omi_backend/workers/memory_jobs.py index fe4b1c19..6b8da757 100644 --- a/backends/advanced/src/advanced_omi_backend/workers/memory_jobs.py +++ b/backends/advanced/src/advanced_omi_backend/workers/memory_jobs.py @@ -46,7 +46,7 @@ async def process_memory_job( Dict with processing results """ from advanced_omi_backend.models.conversation import Conversation - from advanced_omi_backend.memory import get_memory_service + from advanced_omi_backend.services.memory import get_memory_service from advanced_omi_backend.users import get_user_by_id start_time = time.time() @@ -142,7 +142,7 @@ async def process_memory_job( # Determine memory provider from memory service memory_provider = conversation_model.MemoryProvider.FRIEND_LITE # Default try: - from advanced_omi_backend.memory import get_memory_service + from advanced_omi_backend.services.memory import get_memory_service memory_service_obj = get_memory_service() provider_name = memory_service_obj.__class__.__name__ if "OpenMemory" in provider_name: diff --git a/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py b/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py index 9690f286..2fc4c5ab 100644 --- a/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py +++ b/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py @@ -193,6 +193,75 @@ async def transcribe_full_audio_job( logger.info(f"๐Ÿ“Š Transcription complete: {len(transcript_text)} chars, {len(segments)} segments, {len(words)} words") + # Validate meaningful speech BEFORE any further processing + from advanced_omi_backend.utils.conversation_utils import analyze_speech, mark_conversation_deleted + + transcript_data = {"text": transcript_text, "words": words} + speech_analysis = analyze_speech(transcript_data) + + if not speech_analysis.get("has_speech", False): + logger.warning( + f"โš ๏ธ Transcription found no meaningful speech for conversation {conversation_id}: " + f"{speech_analysis.get('reason', 'unknown')}" + ) + + # Mark conversation as deleted + await mark_conversation_deleted( + conversation_id=conversation_id, + deletion_reason="no_meaningful_speech_batch_transcription" + ) + + # Cancel all dependent jobs (cropping, speaker recognition, memory, title/summary) + from rq import get_current_job + from rq.job import Job + + current_job = get_current_job() + if current_job: + # Get all jobs that depend on this transcription job + from advanced_omi_backend.controllers.queue_controller import redis_conn + + # Find dependent jobs by searching for jobs with this job as dependency + try: + # Cancel jobs based on conversation_id pattern + job_patterns = [ + f"crop_{conversation_id[:12]}", + f"speaker_{conversation_id[:12]}", + f"memory_{conversation_id[:12]}", + f"title_summary_{conversation_id[:12]}" + ] + + cancelled_jobs = [] + for job_id in job_patterns: + try: + dependent_job = Job.fetch(job_id, connection=redis_conn) + if dependent_job and dependent_job.get_status() in ['queued', 'deferred', 'scheduled']: + dependent_job.cancel() + cancelled_jobs.append(job_id) + logger.info(f"โœ… Cancelled dependent job: {job_id}") + except Exception as e: + logger.debug(f"Job {job_id} not found or already completed: {e}") + + if cancelled_jobs: + logger.info(f"๐Ÿšซ Cancelled {len(cancelled_jobs)} dependent jobs due to no meaningful speech") + except Exception as cancel_error: + logger.warning(f"Failed to cancel some dependent jobs: {cancel_error}") + + # Return early with failure status + return { + "success": False, + "conversation_id": conversation_id, + "error": "no_meaningful_speech", + "reason": speech_analysis.get("reason"), + "word_count": speech_analysis.get("word_count", 0), + "duration": speech_analysis.get("duration", 0.0), + "deleted": True + } + + logger.info( + f"โœ… Meaningful speech validated: {speech_analysis.get('word_count')} words, " + f"{speech_analysis.get('duration', 0):.1f}s" + ) + # Calculate processing time (transcription only) processing_time = time.time() - start_time diff --git a/extras/mycelia b/extras/mycelia new file mode 160000 index 00000000..ca7b177b --- /dev/null +++ b/extras/mycelia @@ -0,0 +1 @@ +Subproject commit ca7b177b1e9228b63399da557a1ddbf696cf6762 From 74d548271c618a7a8f33306919314817b02a0692 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Mon, 1 Dec 2025 14:14:33 +0000 Subject: [PATCH 02/21] Added support for mycelia --- Makefile | 52 ++- backends/advanced/.env.template | 24 +- backends/advanced/docker-compose-test.yml | 58 +++ .../scripts/create_mycelia_api_key.py | 112 +++++ .../scripts/sync_friendlite_mycelia.py | 382 ++++++++++++++++++ .../src/advanced_omi_backend/app_factory.py | 7 + .../advanced/src/advanced_omi_backend/auth.py | 35 ++ .../controllers/memory_controller.py | 54 ++- .../routers/modules/health_routes.py | 36 ++ .../routers/modules/memory_routes.py | 18 +- .../services/memory/base.py | 13 + .../services/memory/config.py | 19 +- .../services/memory/providers/mycelia.py | 378 ++++++++++++----- .../services/mycelia_sync.py | 248 ++++++++++++ backends/advanced/webui/src/App.tsx | 4 +- .../webui/src/contexts/AuthContext.tsx | 3 + .../webui/src/pages/MemoriesRouter.tsx | 22 + 17 files changed, 1359 insertions(+), 106 deletions(-) create mode 100755 backends/advanced/scripts/create_mycelia_api_key.py create mode 100644 backends/advanced/scripts/sync_friendlite_mycelia.py create mode 100644 backends/advanced/src/advanced_omi_backend/services/mycelia_sync.py create mode 100644 backends/advanced/webui/src/pages/MemoriesRouter.tsx diff --git a/Makefile b/Makefile index 1a5a3829..3d03a180 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ export $(shell sed 's/=.*//' config.env | grep -v '^\s*$$' | grep -v '^\s*\#') SCRIPTS_DIR := scripts K8S_SCRIPTS_DIR := $(SCRIPTS_DIR)/k8s -.PHONY: help menu setup-k8s setup-infrastructure setup-rbac setup-storage-pvc config config-docker config-k8s config-all clean deploy deploy-docker deploy-k8s deploy-k8s-full deploy-infrastructure deploy-apps check-infrastructure check-apps build-backend up-backend down-backend k8s-status k8s-cleanup k8s-purge audio-manage test-robot test-robot-integration test-robot-unit test-robot-endpoints test-robot-specific test-robot-clean +.PHONY: help menu setup-k8s setup-infrastructure setup-rbac setup-storage-pvc config config-docker config-k8s config-all clean deploy deploy-docker deploy-k8s deploy-k8s-full deploy-infrastructure deploy-apps check-infrastructure check-apps build-backend up-backend down-backend k8s-status k8s-cleanup k8s-purge audio-manage mycelia-sync-status mycelia-sync-all mycelia-sync-user mycelia-check-orphans mycelia-reassign-orphans test-robot test-robot-integration test-robot-unit test-robot-endpoints test-robot-specific test-robot-clean # Default target .DEFAULT_GOAL := menu @@ -57,6 +57,13 @@ menu: ## Show interactive menu (default) @echo " check-apps ๐Ÿ” Check application services" @echo " clean ๐Ÿงน Clean up generated files" @echo + @echo "๐Ÿ”„ Mycelia Sync:" + @echo " mycelia-sync-status ๐Ÿ“Š Show Mycelia OAuth sync status" + @echo " mycelia-sync-all ๐Ÿ”„ Sync all Friend-Lite users to Mycelia" + @echo " mycelia-sync-user ๐Ÿ‘ค Sync specific user (EMAIL=user@example.com)" + @echo " mycelia-check-orphans ๐Ÿ” Find orphaned Mycelia objects" + @echo " mycelia-reassign-orphans โ™ป๏ธ Reassign orphans (EMAIL=admin@example.com)" + @echo @echo "Current configuration:" @echo " DOMAIN: $(DOMAIN)" @echo " DEPLOYMENT_MODE: $(DEPLOYMENT_MODE)" @@ -101,6 +108,13 @@ help: ## Show detailed help for all targets @echo "๐ŸŽต AUDIO MANAGEMENT:" @echo " audio-manage Interactive audio file management" @echo + @echo "๐Ÿ”„ MYCELIA SYNC:" + @echo " mycelia-sync-status Show Mycelia OAuth sync status for all users" + @echo " mycelia-sync-all Sync all Friend-Lite users to Mycelia OAuth" + @echo " mycelia-sync-user Sync specific user (EMAIL=user@example.com)" + @echo " mycelia-check-orphans Find Mycelia objects without Friend-Lite owner" + @echo " mycelia-reassign-orphans Reassign orphaned objects (EMAIL=admin@example.com)" + @echo @echo "๐Ÿงช ROBOT FRAMEWORK TESTING:" @echo " test-robot Run all Robot Framework tests" @echo " test-robot-integration Run integration tests only" @@ -333,6 +347,42 @@ audio-manage: ## Interactive audio file management @echo "๐ŸŽต Starting audio file management..." @$(SCRIPTS_DIR)/manage-audio-files.sh +# ======================================== +# MYCELIA SYNC +# ======================================== + +mycelia-sync-status: ## Show Mycelia OAuth sync status for all users + @echo "๐Ÿ“Š Checking Mycelia OAuth sync status..." + @cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --status + +mycelia-sync-all: ## Sync all Friend-Lite users to Mycelia OAuth + @echo "๐Ÿ”„ Syncing all Friend-Lite users to Mycelia OAuth..." + @echo "โš ๏ธ This will create OAuth credentials for users without them" + @read -p "Continue? (y/N): " confirm && [ "$$confirm" = "y" ] || exit 1 + @cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --sync-all + +mycelia-sync-user: ## Sync specific user to Mycelia OAuth (usage: make mycelia-sync-user EMAIL=user@example.com) + @echo "๐Ÿ‘ค Syncing specific user to Mycelia OAuth..." + @if [ -z "$(EMAIL)" ]; then \ + echo "โŒ EMAIL parameter is required. Usage: make mycelia-sync-user EMAIL=user@example.com"; \ + exit 1; \ + fi + @cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --email $(EMAIL) + +mycelia-check-orphans: ## Find Mycelia objects without Friend-Lite owner + @echo "๐Ÿ” Checking for orphaned Mycelia objects..." + @cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --check-orphans + +mycelia-reassign-orphans: ## Reassign orphaned objects to user (usage: make mycelia-reassign-orphans EMAIL=admin@example.com) + @echo "โ™ป๏ธ Reassigning orphaned Mycelia objects..." + @if [ -z "$(EMAIL)" ]; then \ + echo "โŒ EMAIL parameter is required. Usage: make mycelia-reassign-orphans EMAIL=admin@example.com"; \ + exit 1; \ + fi + @echo "โš ๏ธ This will reassign all orphaned objects to: $(EMAIL)" + @read -p "Continue? (y/N): " confirm && [ "$$confirm" = "y" ] || exit 1 + @cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --reassign-orphans --target-email $(EMAIL) + # ======================================== # TESTING TARGETS # ======================================== diff --git a/backends/advanced/.env.template b/backends/advanced/.env.template index 01724f19..60d2c99e 100644 --- a/backends/advanced/.env.template +++ b/backends/advanced/.env.template @@ -99,8 +99,8 @@ QDRANT_BASE_URL=qdrant # MEMORY PROVIDER CONFIGURATION # ======================================== -# Memory Provider: "friend_lite" (default) or "openmemory_mcp" -# +# Memory Provider: "friend_lite" (default), "openmemory_mcp", or "mycelia" +# # Friend-Lite (default): In-house memory system with full control # - Custom LLM-powered extraction with individual fact storage # - Smart deduplication and memory updates (ADD/UPDATE/DELETE) @@ -113,6 +113,13 @@ QDRANT_BASE_URL=qdrant # - Web UI at http://localhost:8765 # - Requires external server setup # +# Mycelia: Full-featured personal memory timeline +# - Voice, screenshots, and text capture +# - Timeline UI with waveform playback +# - Conversation extraction and semantic search +# - OAuth federation for cross-instance sharing +# - Requires Mycelia server setup (extras/mycelia) +# # See MEMORY_PROVIDERS.md for detailed comparison MEMORY_PROVIDER=friend_lite @@ -128,6 +135,19 @@ MEMORY_PROVIDER=friend_lite # OPENMEMORY_USER_ID=openmemory # OPENMEMORY_TIMEOUT=30 +# ---------------------------------------- +# Mycelia Configuration +# (Only needed if MEMORY_PROVIDER=mycelia) +# ---------------------------------------- +# First start Mycelia: +# cd extras/mycelia && docker compose up -d redis mongo mongo-search +# cd extras/mycelia/backend && deno task dev +# +# IMPORTANT: JWT_SECRET in Mycelia backend/.env must match AUTH_SECRET_KEY above +# MYCELIA_URL=http://host.docker.internal:5173 +# MYCELIA_DB=mycelia # Database name (use mycelia_test for test environment) +# MYCELIA_TIMEOUT=30 + # ======================================== # OPTIONAL FEATURES # ======================================== diff --git a/backends/advanced/docker-compose-test.yml b/backends/advanced/docker-compose-test.yml index 029d0238..1dde7c55 100644 --- a/backends/advanced/docker-compose-test.yml +++ b/backends/advanced/docker-compose-test.yml @@ -38,6 +38,8 @@ services: - MEMORY_PROVIDER=${MEMORY_PROVIDER:-friend_lite} - OPENMEMORY_MCP_URL=${OPENMEMORY_MCP_URL:-http://host.docker.internal:8765} - OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory} + - MYCELIA_URL=http://mycelia-backend-test:5173 + - MYCELIA_DB=mycelia_test # Disable speaker recognition in test environment to prevent segment duplication - DISABLE_SPEAKER_RECOGNITION=false - SPEAKER_SERVICE_URL=https://localhost:8085 @@ -146,6 +148,8 @@ services: - MEMORY_PROVIDER=${MEMORY_PROVIDER:-friend_lite} - OPENMEMORY_MCP_URL=${OPENMEMORY_MCP_URL:-http://host.docker.internal:8765} - OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory} + - MYCELIA_URL=http://mycelia-backend-test:5173 + - MYCELIA_DB=mycelia_test - DISABLE_SPEAKER_RECOGNITION=false - SPEAKER_SERVICE_URL=https://localhost:8085 # Set low inactivity timeout for tests (2 seconds instead of 60) @@ -163,6 +167,60 @@ services: condition: service_started restart: unless-stopped + # Mycelia - AI memory and timeline service (test environment) + mycelia-backend-test: + build: + context: ../../extras/mycelia/backend + dockerfile: Dockerfile.simple + ports: + - "5100:5173" # Test backend port + environment: + # Shared JWT secret for Friend-Lite authentication (test key) + - JWT_SECRET=test-jwt-signing-key-for-integration-tests + - SECRET_KEY=test-jwt-signing-key-for-integration-tests + # MongoDB connection (test database) + - MONGO_URL=mongodb://mongo-test:27017 + - MONGO_DB=mycelia_test + - DATABASE_NAME=mycelia_test + # Redis connection (ioredis uses individual host/port, not URL) + - REDIS_HOST=redis-test + - REDIS_PORT=6379 + volumes: + - ../../extras/mycelia/backend/app:/app/app # Mount source for development + depends_on: + mongo-test: + condition: service_healthy + redis-test: + condition: service_started + healthcheck: + test: ["CMD", "deno", "eval", "fetch('http://localhost:5173/health').then(r => r.ok ? Deno.exit(0) : Deno.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + restart: unless-stopped + profiles: + - mycelia + + mycelia-frontend-test: + build: + context: ../../extras/mycelia + dockerfile: frontend/Dockerfile.simple + args: + - VITE_API_URL=http://localhost:5100 + ports: + - "3002:8080" # Nginx serves on 8080 internally + environment: + - VITE_API_URL=http://localhost:5100 + volumes: + - ../../extras/mycelia/frontend/src:/app/src # Mount source for development + depends_on: + mycelia-backend-test: + condition: service_healthy + restart: unless-stopped + profiles: + - mycelia + # caddy: # image: caddy:2-alpine # ports: diff --git a/backends/advanced/scripts/create_mycelia_api_key.py b/backends/advanced/scripts/create_mycelia_api_key.py new file mode 100755 index 00000000..ac2149e8 --- /dev/null +++ b/backends/advanced/scripts/create_mycelia_api_key.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Create a proper Mycelia API key (not OAuth client) for Friend-Lite user.""" + +import os +import sys +import secrets +import hashlib +from pymongo import MongoClient +from bson import ObjectId +from datetime import datetime + +# MongoDB configuration +MONGO_URL = os.getenv("MONGO_URL", "mongodb://localhost:27018") +MYCELIA_DB = os.getenv("MYCELIA_DB", os.getenv("DATABASE_NAME", "mycelia_test")) + +# User ID from JWT or argument +USER_ID = os.getenv("USER_ID", "692c7727c7b16bdf58d23cd1") # test user + + +def hash_api_key_with_salt(api_key: str, salt: bytes) -> str: + """Hash API key with salt (matches Mycelia's hashApiKey function).""" + # SHA256(salt + apiKey) in base64 + import base64 + h = hashlib.sha256() + h.update(salt) + h.update(api_key.encode('utf-8')) + return base64.b64encode(h.digest()).decode('utf-8') # Use base64 like Mycelia + + +def main(): + print(f"๐Ÿ“Š MongoDB Configuration:") + print(f" URL: {MONGO_URL}") + print(f" Database: {MYCELIA_DB}\n") + + print("๐Ÿ” Creating Mycelia API Key\n") + + # Generate API key in Mycelia format: mycelia_{random_base64url} + random_part = secrets.token_urlsafe(32) + api_key = f"mycelia_{random_part}" + + # Generate salt (32 bytes) + salt = secrets.token_bytes(32) + + # Hash the API key with salt + hashed_key = hash_api_key_with_salt(api_key, salt) + + # Open prefix (first 16 chars for fast lookup) + open_prefix = api_key[:16] + + print(f"โœ… Generated API Key:") + print(f" Key: {api_key}") + print(f" Open Prefix: {open_prefix}") + print(f" Owner: {USER_ID}\n") + + # Connect to MongoDB + client = MongoClient(MONGO_URL) + db = client[MYCELIA_DB] + api_keys = db["api_keys"] + + # Check for existing active keys for this user + existing = api_keys.find_one({"owner": USER_ID, "isActive": True}) + if existing: + print(f"โ„น๏ธ Existing active API key found: {existing['_id']}") + print(f" Deactivating old key...\n") + api_keys.update_one( + {"_id": existing["_id"]}, + {"$set": {"isActive": False}} + ) + + # Create API key document (matches Mycelia's format) + import base64 + api_key_doc = { + "hashedKey": hashed_key, # Note: hashedKey, not hash! + "salt": base64.b64encode(salt).decode('utf-8'), # Store as base64 like Mycelia + "owner": USER_ID, + "name": "Friend-Lite Integration", + "policies": [ + { + "resource": "**", + "action": "*", + "effect": "allow" + } + ], + "openPrefix": open_prefix, + "createdAt": datetime.now(), + "isActive": True, + } + + # Insert into database + result = api_keys.insert_one(api_key_doc) + client_id = str(result.inserted_id) + + print(f"๐ŸŽ‰ API Key Created Successfully!") + print(f" Client ID: {client_id}") + print(f" API Key: {api_key}") + print(f"\n" + "=" * 70) + print("๐Ÿ“‹ MYCELIA CONFIGURATION (Test Environment)") + print("=" * 70) + print(f"\n1๏ธโƒฃ Configure Mycelia Frontend Settings:") + print(f" โ€ข Go to: http://localhost:3002/settings") + print(f" โ€ข API Endpoint: http://localhost:5100") + print(f" โ€ข Client ID: {client_id}") + print(f" โ€ข Client Secret: {api_key}") + print(f" โ€ข Click 'Save' and then 'Test Token'") + print(f"\nโœ… This API key uses the proper Mycelia format with salt!") + print("=" * 70 + "\n") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/backends/advanced/scripts/sync_friendlite_mycelia.py b/backends/advanced/scripts/sync_friendlite_mycelia.py new file mode 100644 index 00000000..c7051f2c --- /dev/null +++ b/backends/advanced/scripts/sync_friendlite_mycelia.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python3 +""" +Sync Friend-Lite users with Mycelia OAuth credentials. + +This script helps migrate existing Friend-Lite installations to use Mycelia, +or sync existing Mycelia installations with Friend-Lite users. + +Usage: + # Dry run (preview changes) + python scripts/sync_friendlite_mycelia.py --dry-run + + # Sync all users + python scripts/sync_friendlite_mycelia.py --sync-all + + # Sync specific user + python scripts/sync_friendlite_mycelia.py --email admin@example.com + + # Check for orphaned Mycelia objects + python scripts/sync_friendlite_mycelia.py --check-orphans + + # Reassign orphaned objects to a user + python scripts/sync_friendlite_mycelia.py --reassign-orphans --target-email admin@example.com + +Environment Variables: + MONGODB_URI or MONGO_URL - MongoDB connection string + MYCELIA_DB - Mycelia database name (default: mycelia) +""" + +import os +import sys +import argparse +import secrets +import hashlib +import base64 +from datetime import datetime +from typing import List, Dict, Tuple, Optional +from pymongo import MongoClient +from bson import ObjectId + +# Add parent directory to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + + +class FriendLiteMyceliaSync: + """Sync Friend-Lite users with Mycelia OAuth credentials.""" + + def __init__(self, mongo_url: str, mycelia_db: str, friendlite_db: str): + self.mongo_url = mongo_url + self.mycelia_db = mycelia_db + self.friendlite_db = friendlite_db + self.client = MongoClient(mongo_url) + + print(f"๐Ÿ“Š Connected to MongoDB:") + print(f" URL: {mongo_url}") + print(f" Friend-Lite DB: {friendlite_db}") + print(f" Mycelia DB: {mycelia_db}\n") + + def _hash_api_key_with_salt(self, api_key: str, salt: bytes) -> str: + """Hash API key with salt (matches Mycelia's implementation).""" + h = hashlib.sha256() + h.update(salt) + h.update(api_key.encode('utf-8')) + return base64.b64encode(h.digest()).decode('utf-8') + + def get_all_friendlite_users(self) -> List[Dict]: + """Get all users from Friend-Lite database.""" + db = self.client[self.friendlite_db] + users = list(db["users"].find({})) + return users + + def get_all_mycelia_objects(self) -> List[Dict]: + """Get all objects from Mycelia database.""" + db = self.client[self.mycelia_db] + objects = list(db["objects"].find({})) + return objects + + def get_mycelia_api_key_for_user(self, user_id: str) -> Optional[Dict]: + """Check if user already has a Mycelia API key.""" + db = self.client[self.mycelia_db] + api_key = db["api_keys"].find_one({ + "owner": user_id, + "isActive": True + }) + return api_key + + def create_mycelia_api_key(self, user_id: str, user_email: str, dry_run: bool = False) -> Tuple[str, str]: + """Create a Mycelia API key for a Friend-Lite user.""" + # Generate API key + random_part = secrets.token_urlsafe(32) + api_key = f"mycelia_{random_part}" + salt = secrets.token_bytes(32) + hashed_key = self._hash_api_key_with_salt(api_key, salt) + open_prefix = api_key[:16] + + api_key_doc = { + "hashedKey": hashed_key, + "salt": base64.b64encode(salt).decode('utf-8'), + "owner": user_id, + "name": f"Friend-Lite Auto ({user_email})", + "policies": [{"resource": "**", "action": "*", "effect": "allow"}], + "openPrefix": open_prefix, + "createdAt": datetime.utcnow(), + "isActive": True, + } + + if dry_run: + print(f" [DRY RUN] Would create API key with owner={user_id}") + return "dry-run-client-id", "dry-run-api-key" + + db = self.client[self.mycelia_db] + result = db["api_keys"].insert_one(api_key_doc) + client_id = str(result.inserted_id) + + # Update Friend-Lite user document + fl_db = self.client[self.friendlite_db] + fl_db["users"].update_one( + {"_id": ObjectId(user_id)}, + { + "$set": { + "mycelia_oauth": { + "client_id": client_id, + "created_at": datetime.utcnow(), + "synced": True + } + } + } + ) + + return client_id, api_key + + def sync_user(self, user: Dict, dry_run: bool = False) -> bool: + """Sync a single user to Mycelia OAuth.""" + user_id = str(user["_id"]) + user_email = user.get("email", "unknown") + + # Check if already synced + existing = self.get_mycelia_api_key_for_user(user_id) + if existing: + print(f"โœ“ {user_email:40} Already synced (Client ID: {existing['_id']})") + return False + + # Create new API key + try: + client_id, api_key = self.create_mycelia_api_key(user_id, user_email, dry_run) + + if dry_run: + print(f"โ†’ {user_email:40} [DRY RUN] Would create OAuth credentials") + else: + print(f"โœ“ {user_email:40} Created OAuth credentials") + print(f" Client ID: {client_id}") + print(f" Client Secret: {api_key}") + + return True + except Exception as e: + print(f"โœ— {user_email:40} Failed: {e}") + return False + + def sync_all_users(self, dry_run: bool = False): + """Sync all Friend-Lite users to Mycelia OAuth.""" + users = self.get_all_friendlite_users() + + print(f"{'='*80}") + print(f"SYNC ALL USERS") + print(f"{'='*80}") + print(f"Found {len(users)} Friend-Lite users\n") + + if dry_run: + print("๐Ÿ” DRY RUN MODE - No changes will be made\n") + + synced_count = 0 + for user in users: + if self.sync_user(user, dry_run): + synced_count += 1 + + print(f"\n{'='*80}") + if dry_run: + print(f"DRY RUN SUMMARY: Would sync {synced_count} users") + else: + print(f"SUMMARY: Synced {synced_count} new users, {len(users) - synced_count} already synced") + print(f"{'='*80}\n") + + def check_orphaned_objects(self): + """Find Mycelia objects with userId not matching any Friend-Lite user.""" + users = self.get_all_friendlite_users() + user_ids = {str(user["_id"]) for user in users} + + objects = self.get_all_mycelia_objects() + + print(f"{'='*80}") + print(f"ORPHANED OBJECTS CHECK") + print(f"{'='*80}") + print(f"Friend-Lite users: {len(user_ids)}") + print(f"Mycelia objects: {len(objects)}\n") + + orphaned = [] + user_object_counts = {} + + for obj in objects: + obj_user_id = obj.get("userId") + if obj_user_id: + # Count objects per user + user_object_counts[obj_user_id] = user_object_counts.get(obj_user_id, 0) + 1 + + # Check if orphaned + if obj_user_id not in user_ids: + orphaned.append(obj) + + # Display object distribution + print("Object distribution by userId:") + for user_id, count in sorted(user_object_counts.items(), key=lambda x: x[1], reverse=True): + status = "โœ“" if user_id in user_ids else "โœ— ORPHANED" + print(f" {user_id}: {count:4} objects {status}") + + # Display orphaned objects + if orphaned: + print(f"\nโš ๏ธ Found {len(orphaned)} orphaned objects:") + for obj in orphaned[:10]: # Show first 10 + obj_id = obj.get("_id") + obj_name = obj.get("name", "Unnamed")[:50] + obj_user_id = obj.get("userId") + print(f" {obj_id} - {obj_name} (userId: {obj_user_id})") + + if len(orphaned) > 10: + print(f" ... and {len(orphaned) - 10} more") + else: + print("\nโœ“ No orphaned objects found!") + + print(f"{'='*80}\n") + return orphaned + + def reassign_orphaned_objects(self, target_email: str, dry_run: bool = False): + """Reassign all orphaned objects to a specific Friend-Lite user.""" + # Get target user + fl_db = self.client[self.friendlite_db] + target_user = fl_db["users"].find_one({"email": target_email}) + + if not target_user: + print(f"โœ— User with email '{target_email}' not found in Friend-Lite") + return + + target_user_id = str(target_user["_id"]) + print(f"Target user: {target_email} (ID: {target_user_id})\n") + + # Find orphaned objects + users = self.get_all_friendlite_users() + user_ids = {str(user["_id"]) for user in users} + objects = self.get_all_mycelia_objects() + + orphaned = [obj for obj in objects if obj.get("userId") and obj.get("userId") not in user_ids] + + if not orphaned: + print("โœ“ No orphaned objects to reassign") + return + + print(f"{'='*80}") + print(f"REASSIGN ORPHANED OBJECTS") + print(f"{'='*80}") + print(f"Found {len(orphaned)} orphaned objects") + + if dry_run: + print("๐Ÿ” DRY RUN MODE - No changes will be made\n") + else: + print(f"Will reassign to: {target_email}\n") + + mycelia_db = self.client[self.mycelia_db] + + for obj in orphaned: + obj_id = obj["_id"] + old_user_id = obj.get("userId") + obj_name = obj.get("name", "Unnamed")[:50] + + if dry_run: + print(f"โ†’ [DRY RUN] Would reassign: {obj_name}") + print(f" From: {old_user_id} โ†’ To: {target_user_id}") + else: + result = mycelia_db["objects"].update_one( + {"_id": obj_id}, + {"$set": {"userId": target_user_id}} + ) + if result.modified_count > 0: + print(f"โœ“ Reassigned: {obj_name}") + else: + print(f"โœ— Failed to reassign: {obj_name}") + + print(f"\n{'='*80}") + if dry_run: + print(f"DRY RUN SUMMARY: Would reassign {len(orphaned)} objects to {target_email}") + else: + print(f"SUMMARY: Reassigned {len(orphaned)} objects to {target_email}") + print(f"{'='*80}\n") + + def display_sync_status(self): + """Display current sync status.""" + users = self.get_all_friendlite_users() + + print(f"{'='*80}") + print(f"SYNC STATUS") + print(f"{'='*80}\n") + + synced_count = 0 + unsynced_count = 0 + + print(f"{'Email':<40} {'User ID':<30} {'Status'}") + print(f"{'-'*40} {'-'*30} {'-'*20}") + + for user in users: + user_id = str(user["_id"]) + user_email = user.get("email", "unknown") + + existing = self.get_mycelia_api_key_for_user(user_id) + if existing: + status = f"โœ“ Synced (Client ID: {existing['_id']})" + synced_count += 1 + else: + status = "โœ— Not synced" + unsynced_count += 1 + + print(f"{user_email:<40} {user_id:<30} {status}") + + print(f"\n{'='*80}") + print(f"Total users: {len(users)}") + print(f"Synced: {synced_count}") + print(f"Not synced: {unsynced_count}") + print(f"{'='*80}\n") + + +def main(): + parser = argparse.ArgumentParser( + description="Sync Friend-Lite users with Mycelia OAuth credentials", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__ + ) + + parser.add_argument("--dry-run", action="store_true", help="Preview changes without making them") + parser.add_argument("--sync-all", action="store_true", help="Sync all Friend-Lite users") + parser.add_argument("--email", type=str, help="Sync specific user by email") + parser.add_argument("--check-orphans", action="store_true", help="Check for orphaned Mycelia objects") + parser.add_argument("--reassign-orphans", action="store_true", help="Reassign orphaned objects to target user") + parser.add_argument("--target-email", type=str, help="Target user email for reassigning orphans") + parser.add_argument("--status", action="store_true", help="Display current sync status") + + args = parser.parse_args() + + # Get configuration from environment + mongo_url = os.getenv("MONGODB_URI") or os.getenv("MONGO_URL", "mongodb://localhost:27017") + + # Extract database name from MONGODB_URI if present + if "/" in mongo_url and mongo_url.count("/") >= 3: + friendlite_db = mongo_url.split("/")[-1].split("?")[0] or "friend-lite" + else: + friendlite_db = "friend-lite" + + mycelia_db = os.getenv("MYCELIA_DB", os.getenv("DATABASE_NAME", "mycelia")) + + # Create sync service + sync = FriendLiteMyceliaSync(mongo_url, mycelia_db, friendlite_db) + + # Execute requested action + if args.status: + sync.display_sync_status() + elif args.sync_all: + sync.sync_all_users(dry_run=args.dry_run) + elif args.email: + fl_db = sync.client[friendlite_db] + user = fl_db["users"].find_one({"email": args.email}) + if user: + sync.sync_user(user, dry_run=args.dry_run) + else: + print(f"โœ— User with email '{args.email}' not found") + elif args.check_orphans: + sync.check_orphaned_objects() + elif args.reassign_orphans: + if not args.target_email: + print("โœ— --target-email required for --reassign-orphans") + sys.exit(1) + sync.reassign_orphaned_objects(args.target_email, dry_run=args.dry_run) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/backends/advanced/src/advanced_omi_backend/app_factory.py b/backends/advanced/src/advanced_omi_backend/app_factory.py index 8aa0c97a..65b1adbf 100644 --- a/backends/advanced/src/advanced_omi_backend/app_factory.py +++ b/backends/advanced/src/advanced_omi_backend/app_factory.py @@ -73,6 +73,13 @@ async def lifespan(app: FastAPI): application_logger.error(f"Failed to create admin user: {e}") # Don't raise here as this is not critical for startup + # Sync admin user with Mycelia OAuth (if using Mycelia memory provider) + try: + from advanced_omi_backend.services.mycelia_sync import sync_admin_on_startup + await sync_admin_on_startup() + except Exception as e: + application_logger.error(f"Failed to sync admin with Mycelia OAuth: {e}") + # Don't raise here as this is not critical for startup # Initialize Redis connection for RQ try: diff --git a/backends/advanced/src/advanced_omi_backend/auth.py b/backends/advanced/src/advanced_omi_backend/auth.py index a39637f1..8b489988 100644 --- a/backends/advanced/src/advanced_omi_backend/auth.py +++ b/backends/advanced/src/advanced_omi_backend/auth.py @@ -98,6 +98,41 @@ def get_jwt_strategy() -> JWTStrategy: ) # 24 hours for device compatibility +def generate_jwt_for_user(user_id: str, user_email: str) -> str: + """Generate a JWT token for a user to authenticate with external services. + + This function creates a JWT token that can be used to authenticate with + services that share the same AUTH_SECRET_KEY, such as Mycelia. + + Args: + user_id: User's unique identifier (MongoDB ObjectId as string) + user_email: User's email address + + Returns: + JWT token string valid for 24 hours + + Example: + >>> token = generate_jwt_for_user("507f1f77bcf86cd799439011", "user@example.com") + >>> # Use token to call Mycelia API + """ + from datetime import datetime, timedelta + import jwt + + # Create JWT payload matching Friend-Lite's standard format + payload = { + "sub": user_id, # Subject = user ID + "email": user_email, + "iss": "friend-lite", # Issuer + "aud": "friend-lite", # Audience + "exp": datetime.utcnow() + timedelta(hours=24), # 24 hour expiration + "iat": datetime.utcnow(), # Issued at + } + + # Sign the token with the same secret key + token = jwt.encode(payload, SECRET_KEY, algorithm="HS256") + return token + + # Authentication backends cookie_backend = AuthenticationBackend( name="cookie", diff --git a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py index f6ca8387..d917ec18 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py @@ -27,13 +27,16 @@ async def get_memories(user: User, limit: int, user_id: Optional[str] = None): # Execute memory retrieval directly (now async) memories = await memory_service.get_all_memories(target_user_id, limit) - + # Get total count (service returns None on failure) total_count = await memory_service.count_memories(target_user_id) + # Convert MemoryEntry objects to dicts for JSON serialization + memories_dicts = [mem.to_dict() if hasattr(mem, 'to_dict') else mem for mem in memories] + return { - "memories": memories, - "count": len(memories), + "memories": memories_dicts, + "count": len(memories), "total_count": total_count, "user_id": target_user_id } @@ -87,9 +90,12 @@ async def search_memories(query: str, user: User, limit: int, score_threshold: f # Execute search directly (now async) search_results = await memory_service.search_memories(query, target_user_id, limit, score_threshold) + # Convert MemoryEntry objects to dicts for JSON serialization + results_dicts = [result.to_dict() if hasattr(result, 'to_dict') else result for result in search_results] + return { "query": query, - "results": search_results, + "results": results_dicts, "count": len(search_results), "user_id": target_user_id, } @@ -157,6 +163,46 @@ async def get_memories_unfiltered(user: User, limit: int, user_id: Optional[str] ) +async def add_memory(content: str, user: User, source_id: Optional[str] = None): + """Add a memory directly from content text. Extracts structured memories from the provided content.""" + try: + memory_service = get_memory_service() + + # Use source_id or generate a unique one + memory_source_id = source_id or f"manual_{user.user_id}_{int(asyncio.get_event_loop().time())}" + + # Extract memories from content + success, memory_ids = await memory_service.add_memory( + transcript=content, + client_id=f"{user.user_id[:8]}-manual", + source_id=memory_source_id, + user_id=user.user_id, + user_email=user.email, + allow_update=False, + db_helper=None + ) + + if success: + return { + "success": True, + "memory_ids": memory_ids, + "count": len(memory_ids), + "source_id": memory_source_id, + "message": f"Successfully created {len(memory_ids)} memory/memories" + } + else: + return JSONResponse( + status_code=500, + content={"success": False, "message": "Failed to create memories"} + ) + + except Exception as e: + audio_logger.error(f"Error adding memory: {e}", exc_info=True) + return JSONResponse( + status_code=500, content={"success": False, "message": f"Error adding memory: {str(e)}"} + ) + + async def get_all_memories_admin(user: User, limit: int): """Get all memories across all users for admin review. Admin only.""" try: diff --git a/backends/advanced/src/advanced_omi_backend/routers/modules/health_routes.py b/backends/advanced/src/advanced_omi_backend/routers/modules/health_routes.py index 1634bc3d..06e0da1e 100644 --- a/backends/advanced/src/advanced_omi_backend/routers/modules/health_routes.py +++ b/backends/advanced/src/advanced_omi_backend/routers/modules/health_routes.py @@ -273,6 +273,42 @@ async def health_check(): "provider": "openmemory_mcp", "critical": False, } + elif memory_provider == "mycelia": + # Mycelia memory service check + try: + # Test Mycelia memory service connection with timeout + test_success = await asyncio.wait_for(memory_service.test_connection(), timeout=8.0) + if test_success: + health_status["services"]["memory_service"] = { + "status": "โœ… Mycelia Memory Connected", + "healthy": True, + "provider": "mycelia", + "critical": False, + } + else: + health_status["services"]["memory_service"] = { + "status": "โš ๏ธ Mycelia Memory Test Failed", + "healthy": False, + "provider": "mycelia", + "critical": False, + } + overall_healthy = False + except asyncio.TimeoutError: + health_status["services"]["memory_service"] = { + "status": "โš ๏ธ Mycelia Memory Timeout (8s) - Check Mycelia service", + "healthy": False, + "provider": "mycelia", + "critical": False, + } + overall_healthy = False + except Exception as e: + health_status["services"]["memory_service"] = { + "status": f"โš ๏ธ Mycelia Memory Failed: {str(e)}", + "healthy": False, + "provider": "mycelia", + "critical": False, + } + overall_healthy = False else: health_status["services"]["memory_service"] = { "status": f"โŒ Unknown memory provider: {memory_provider}", diff --git a/backends/advanced/src/advanced_omi_backend/routers/modules/memory_routes.py b/backends/advanced/src/advanced_omi_backend/routers/modules/memory_routes.py index 4d71ce6d..c9bc75e3 100644 --- a/backends/advanced/src/advanced_omi_backend/routers/modules/memory_routes.py +++ b/backends/advanced/src/advanced_omi_backend/routers/modules/memory_routes.py @@ -7,7 +7,8 @@ import logging from typing import Optional -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Query, Body +from pydantic import BaseModel from advanced_omi_backend.auth import current_active_user, current_superuser from advanced_omi_backend.controllers import memory_controller @@ -18,6 +19,12 @@ router = APIRouter(prefix="/memories", tags=["memories"]) +class AddMemoryRequest(BaseModel): + """Request model for adding a memory.""" + content: str + source_id: Optional[str] = None + + @router.get("") async def get_memories( current_user: User = Depends(current_active_user), @@ -50,6 +57,15 @@ async def search_memories( return await memory_controller.search_memories(query, current_user, limit, score_threshold, user_id) +@router.post("") +async def add_memory( + request: AddMemoryRequest, + current_user: User = Depends(current_active_user) +): + """Add a memory directly from content text. The service will extract structured memories from the provided content.""" + return await memory_controller.add_memory(request.content, current_user, request.source_id) + + @router.delete("/{memory_id}") async def delete_memory(memory_id: str, current_user: User = Depends(current_active_user)): """Delete a memory by ID. Users can only delete their own memories, admins can delete any.""" diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/base.py b/backends/advanced/src/advanced_omi_backend/services/memory/base.py index 65d39d75..f205ecdb 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/base.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/base.py @@ -49,6 +49,19 @@ def __post_init__(self): if self.created_at is None: self.created_at = str(int(time.time())) + def to_dict(self) -> Dict[str, Any]: + """Convert MemoryEntry to dictionary for JSON serialization.""" + return { + "id": self.id, + "memory": self.content, # Frontend expects 'memory' key + "content": self.content, # Also provide 'content' for consistency + "metadata": self.metadata, + "embedding": self.embedding, + "score": self.score, + "created_at": self.created_at, + "user_id": self.metadata.get("user_id") # Extract user_id from metadata + } + class MemoryServiceBase(ABC): """Abstract base class defining the core memory service interface. diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/config.py b/backends/advanced/src/advanced_omi_backend/services/memory/config.py index ae03fcd8..9d5c8324 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/config.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/config.py @@ -159,14 +159,29 @@ def build_memory_config_from_env() -> MemoryConfig: user_id=os.getenv("OPENMEMORY_USER_ID", "default"), timeout=int(os.getenv("OPENMEMORY_TIMEOUT", "30")) ) - + memory_logger.info(f"๐Ÿ”ง Memory config: Provider=OpenMemory MCP, URL={openmemory_config['server_url']}") - + return MemoryConfig( memory_provider=memory_provider_enum, openmemory_config=openmemory_config, timeout_seconds=int(os.getenv("OPENMEMORY_TIMEOUT", "30")) ) + + # For Mycelia provider, configuration is simple - just URL + if memory_provider_enum == MemoryProvider.MYCELIA: + mycelia_config = create_mycelia_config( + api_url=os.getenv("MYCELIA_URL", "http://localhost:5173"), + timeout=int(os.getenv("MYCELIA_TIMEOUT", "30")) + ) + + memory_logger.info(f"๐Ÿ”ง Memory config: Provider=Mycelia, URL={mycelia_config['api_url']}") + + return MemoryConfig( + memory_provider=memory_provider_enum, + mycelia_config=mycelia_config, + timeout_seconds=int(os.getenv("MYCELIA_TIMEOUT", "30")) + ) # For Friend-Lite provider, use existing complex configuration # Import config loader diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py index ccf30160..3033c307 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py @@ -5,7 +5,9 @@ """ import logging -from typing import Any, List, Optional, Tuple +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple +import httpx from ..base import MemoryEntry, MemoryServiceBase @@ -16,11 +18,10 @@ class MyceliaMemoryService(MemoryServiceBase): """Memory service implementation using Mycelia backend. This class implements the MemoryServiceBase interface by delegating memory - operations to a Mycelia server. + operations to a Mycelia server using JWT authentication from Friend-Lite. Args: api_url: Mycelia API endpoint URL - api_key: Optional API key for authentication timeout: Request timeout in seconds **kwargs: Additional configuration parameters """ @@ -28,7 +29,6 @@ class MyceliaMemoryService(MemoryServiceBase): def __init__( self, api_url: str = "http://localhost:8080", - api_key: Optional[str] = None, timeout: int = 30, **kwargs ): @@ -36,27 +36,34 @@ def __init__( Args: api_url: Mycelia API endpoint - api_key: Optional API key for authentication timeout: Request timeout in seconds **kwargs: Additional configuration parameters """ - self.api_url = api_url - self.api_key = api_key + self.api_url = api_url.rstrip("/") self.timeout = timeout self.config = kwargs self._initialized = False + self._client: Optional[httpx.AsyncClient] = None memory_logger.info(f"๐Ÿ„ Initializing Mycelia memory service at {api_url}") async def initialize(self) -> None: """Initialize Mycelia client and verify connection.""" try: - # TODO: Initialize your Mycelia client here - # Example: self.client = MyceliaClient(self.api_url, self.api_key) - - # Test connection - if not await self.test_connection(): - raise RuntimeError("Failed to connect to Mycelia service") + # Initialize HTTP client + self._client = httpx.AsyncClient( + base_url=self.api_url, + timeout=self.timeout, + headers={"Content-Type": "application/json"} + ) + + # Test connection directly (without calling test_connection to avoid recursion) + try: + response = await self._client.get("/health") + if response.status_code != 200: + raise RuntimeError(f"Health check failed with status {response.status_code}") + except httpx.HTTPError as e: + raise RuntimeError(f"Failed to connect to Mycelia service: {e}") self._initialized = True memory_logger.info("โœ… Mycelia memory service initialized successfully") @@ -65,6 +72,109 @@ async def initialize(self) -> None: memory_logger.error(f"โŒ Failed to initialize Mycelia service: {e}") raise RuntimeError(f"Mycelia initialization failed: {e}") + async def _get_user_jwt(self, user_id: str, user_email: Optional[str] = None) -> str: + """Get JWT token for a user (with optional user lookup). + + Args: + user_id: User ID + user_email: Optional user email (will lookup if not provided) + + Returns: + JWT token string + + Raises: + ValueError: If user not found + """ + from advanced_omi_backend.auth import generate_jwt_for_user + + # If email not provided, lookup user + if not user_email: + from advanced_omi_backend.users import User + user = await User.get(user_id) + if not user: + raise ValueError(f"User {user_id} not found") + user_email = user.email + + return generate_jwt_for_user(user_id, user_email) + + @staticmethod + def _extract_bson_id(raw_id: Any) -> str: + """Extract ID from Mycelia BSON format {"$oid": "..."} or plain string.""" + if isinstance(raw_id, dict) and "$oid" in raw_id: + return raw_id["$oid"] + return str(raw_id) + + @staticmethod + def _extract_bson_date(date_obj: Any) -> Any: + """Extract date from Mycelia BSON format {"$date": "..."} or plain value.""" + if isinstance(date_obj, dict) and "$date" in date_obj: + return date_obj["$date"] + return date_obj + + def _mycelia_object_to_memory_entry(self, obj: Dict, user_id: str) -> MemoryEntry: + """Convert Mycelia object to MemoryEntry. + + Args: + obj: Mycelia object from API + user_id: User ID for metadata + + Returns: + MemoryEntry object + """ + memory_id = self._extract_bson_id(obj.get("_id", "")) + memory_content = obj.get("details", "") + + return MemoryEntry( + id=memory_id, + content=memory_content, + metadata={ + "user_id": user_id, + "name": obj.get("name", ""), + "aliases": obj.get("aliases", []), + "created_at": self._extract_bson_date(obj.get("createdAt")), + "updated_at": self._extract_bson_date(obj.get("updatedAt")), + }, + created_at=self._extract_bson_date(obj.get("createdAt")) + ) + + async def _call_resource( + self, + action: str, + jwt_token: str, + **params + ) -> Dict[str, Any]: + """Call Mycelia objects resource with JWT authentication. + + Args: + action: Action to perform (create, list, get, delete, etc.) + jwt_token: User's JWT token from Friend-Lite + **params: Additional parameters for the action + + Returns: + Response data from Mycelia + + Raises: + RuntimeError: If API call fails + """ + if not self._client: + raise RuntimeError("Mycelia client not initialized") + + try: + response = await self._client.post( + "/api/resource/tech.mycelia.objects", + json={"action": action, **params}, + headers={"Authorization": f"Bearer {jwt_token}"} + ) + response.raise_for_status() + return response.json() + + except httpx.HTTPStatusError as e: + memory_logger.error(f"Mycelia API error: {e.response.status_code} - {e.response.text}") + raise RuntimeError(f"Mycelia API error: {e.response.status_code}") + except Exception as e: + memory_logger.error(f"Failed to call Mycelia resource: {e}") + raise RuntimeError(f"Mycelia API call failed: {e}") + async def add_memory( self, transcript: str, @@ -90,21 +200,37 @@ async def add_memory( Tuple of (success: bool, created_memory_ids: List[str]) """ try: - # TODO: Implement your Mycelia API call to add memories - # Example implementation: - # response = await self.client.add_memories( - # transcript=transcript, - # user_id=user_id, - # metadata={ - # "client_id": client_id, - # "source_id": source_id, - # "user_email": user_email, - # } - # ) - # return (True, response.memory_ids) - - memory_logger.warning("Mycelia add_memory not yet implemented") - return (False, []) + # Generate JWT token for this user + jwt_token = await self._get_user_jwt(user_id, user_email) + + # Create a Mycelia object for this memory + # Memory content is stored in the 'details' field + memory_preview = transcript[:50] + ("..." if len(transcript) > 50 else "") + + object_data = { + "name": f"Memory: {memory_preview}", + "details": transcript, + "aliases": [source_id, client_id], # Searchable by source or client + "isPerson": False, + "isPromise": False, + "isEvent": False, + "isRelationship": False, + # Note: userId is auto-injected by Mycelia from JWT + } + + result = await self._call_resource( + action="create", + jwt_token=jwt_token, + object=object_data + ) + + memory_id = result.get("insertedId") + if memory_id: + memory_logger.info(f"โœ… Created Mycelia memory object: {memory_id}") + return (True, [memory_id]) + else: + memory_logger.error("Failed to create Mycelia memory: no insertedId returned") + return (False, []) except Exception as e: memory_logger.error(f"Failed to add memory via Mycelia: {e}") @@ -124,28 +250,39 @@ async def search_memories( Returns: List of matching MemoryEntry objects ordered by relevance """ + if not self._initialized: + await self.initialize() + try: - # TODO: Implement Mycelia search - # Example implementation: - # results = await self.client.search( - # query=query, - # user_id=user_id, - # limit=limit, - # threshold=score_threshold - # ) - # return [ - # MemoryEntry( - # id=r.id, - # memory=r.text, - # user_id=user_id, - # metadata=r.metadata, - # score=r.score - # ) - # for r in results - # ] - - memory_logger.warning("Mycelia search_memories not yet implemented") - return [] + # Generate JWT token for this user + jwt_token = await self._get_user_jwt(user_id) + + # Search using Mycelia's list action with searchTerm option + result = await self._call_resource( + action="list", + jwt_token=jwt_token, + filters={}, # Auto-scoped by userId in Mycelia + options={ + "searchTerm": query, + "limit": limit, + "sort": {"updatedAt": -1} # Most recent first + } + ) + + # Convert Mycelia objects to MemoryEntry objects + memories = [] + for i, obj in enumerate(result): + # Calculate a simple relevance score (0-1) based on position + # (Mycelia doesn't provide semantic similarity scores yet) + score = 1.0 - (i * 0.1) # Decaying score + if score < score_threshold: + continue + + entry = self._mycelia_object_to_memory_entry(obj, user_id) + entry.score = score # Override score + memories.append(entry) + + return memories except Exception as e: memory_logger.error(f"Failed to search memories via Mycelia: {e}") @@ -163,22 +300,27 @@ async def get_all_memories( Returns: List of MemoryEntry objects for the user """ + if not self._initialized: + await self.initialize() + try: - # TODO: Implement Mycelia get all - # Example implementation: - # results = await self.client.get_all(user_id=user_id, limit=limit) - # return [ - # MemoryEntry( - # id=r.id, - # memory=r.text, - # user_id=user_id, - # metadata=r.metadata - # ) - # for r in results - # ] - - memory_logger.warning("Mycelia get_all_memories not yet implemented") - return [] + # Generate JWT token for this user + jwt_token = await self._get_user_jwt(user_id) + + # List all objects for this user (auto-scoped by Mycelia) + result = await self._call_resource( + action="list", + jwt_token=jwt_token, + filters={}, # Auto-scoped by userId + options={ + "limit": limit, + "sort": {"updatedAt": -1} # Most recent first + } + ) + + # Convert Mycelia objects to MemoryEntry objects + memories = [self._mycelia_object_to_memory_entry(obj, user_id) for obj in result] + return memories except Exception as e: memory_logger.error(f"Failed to get memories via Mycelia: {e}") @@ -193,34 +335,67 @@ async def count_memories(self, user_id: str) -> Optional[int]: Returns: Total count of memories for the user, or None if not supported """ - try: - # TODO: Implement if Mycelia supports efficient counting - # Example: - # return await self.client.count(user_id=user_id) + if not self._initialized: + await self.initialize() - return None # Not implemented yet + try: + # Generate JWT token for this user + jwt_token = await self._get_user_jwt(user_id) + + # Use Mycelia's mongo resource to count objects for this user + if not self._client: + raise RuntimeError("Mycelia client not initialized") + + response = await self._client.post( + "/api/resource/tech.mycelia.mongo", + json={ + "action": "count", + "collection": "objects", + "query": {"userId": user_id} + }, + headers={"Authorization": f"Bearer {jwt_token}"} + ) + response.raise_for_status() + return response.json() except Exception as e: memory_logger.error(f"Failed to count memories via Mycelia: {e}") return None - async def delete_memory(self, memory_id: str) -> bool: + async def delete_memory(self, memory_id: str, user_id: Optional[str] = None, user_email: Optional[str] = None) -> bool: """Delete a specific memory from Mycelia. Args: memory_id: Unique identifier of the memory to delete + user_id: Optional user identifier for authentication + user_email: Optional user email for authentication Returns: True if successfully deleted, False otherwise """ try: - # TODO: Implement Mycelia delete - # Example: - # success = await self.client.delete(memory_id=memory_id) - # return success - - memory_logger.warning("Mycelia delete_memory not yet implemented") - return False + # Need user credentials for JWT - if not provided, we can't delete + if not user_id: + memory_logger.error("User ID required for Mycelia delete operation") + return False + + # Generate JWT token for this user + jwt_token = await self._get_user_jwt(user_id, user_email) + + # Delete the object (auto-scoped by userId in Mycelia) + result = await self._call_resource( + action="delete", + jwt_token=jwt_token, + id=memory_id + ) + + deleted_count = result.get("deletedCount", 0) + if deleted_count > 0: + memory_logger.info(f"โœ… Deleted Mycelia memory object: {memory_id}") + return True + else: + memory_logger.warning(f"No memory deleted with ID: {memory_id}") + return False except Exception as e: memory_logger.error(f"Failed to delete memory via Mycelia: {e}") @@ -236,13 +411,26 @@ async def delete_all_user_memories(self, user_id: str) -> int: Number of memories that were deleted """ try: - # TODO: Implement Mycelia bulk delete - # Example: - # count = await self.client.delete_all(user_id=user_id) - # return count - - memory_logger.warning("Mycelia delete_all_user_memories not yet implemented") - return 0 + # Generate JWT token for this user + jwt_token = await self._get_user_jwt(user_id) + + # First, get all memory IDs for this user + result = await self._call_resource( + action="list", + jwt_token=jwt_token, + filters={}, # Auto-scoped by userId + options={"limit": 10000} # Large limit to get all + ) + + # Delete each memory individually + deleted_count = 0 + for obj in result: + memory_id = self._extract_bson_id(obj.get("_id", "")) + if await self.delete_memory(memory_id, user_id): + deleted_count += 1 + + memory_logger.info(f"โœ… Deleted {deleted_count} Mycelia memories for user {user_id}") + return deleted_count except Exception as e: memory_logger.error(f"Failed to delete user memories via Mycelia: {e}") @@ -255,13 +443,15 @@ async def test_connection(self) -> bool: True if connection is healthy, False otherwise """ try: - # TODO: Implement health check - # Example: - # return await self.client.health_check() + if not self._initialized: + await self.initialize() + + if not self._client: + return False - # For now, just check if URL is set - memory_logger.warning("Mycelia test_connection not fully implemented (stub)") - return self.api_url is not None + # Test connection by hitting a lightweight endpoint + response = await self._client.get("/health") + return response.status_code == 200 except Exception as e: memory_logger.error(f"Mycelia connection test failed: {e}") @@ -270,8 +460,8 @@ async def test_connection(self) -> bool: def shutdown(self) -> None: """Shutdown Mycelia client and cleanup resources.""" memory_logger.info("Shutting down Mycelia memory service") - # TODO: Cleanup if needed - # Example: - # if hasattr(self, 'client'): - # self.client.close() + if self._client: + # Note: httpx AsyncClient should be closed in an async context + # In practice, this will be called during shutdown so we log a warning + memory_logger.warning("HTTP client should be closed with await client.aclose()") self._initialized = False diff --git a/backends/advanced/src/advanced_omi_backend/services/mycelia_sync.py b/backends/advanced/src/advanced_omi_backend/services/mycelia_sync.py new file mode 100644 index 00000000..dd94bf63 --- /dev/null +++ b/backends/advanced/src/advanced_omi_backend/services/mycelia_sync.py @@ -0,0 +1,248 @@ +""" +Mycelia OAuth Synchronization Service. + +This module synchronizes Friend-Lite users with Mycelia OAuth API keys, +ensuring that when users access Mycelia directly, they use credentials +that map to their Friend-Lite user ID. +""" + +import logging +import os +import secrets +import hashlib +import base64 +from typing import Optional, Tuple +from pymongo import MongoClient +from datetime import datetime + +logger = logging.getLogger(__name__) + + +class MyceliaSyncService: + """Synchronize Friend-Lite users with Mycelia OAuth API keys.""" + + def __init__(self): + """Initialize the sync service.""" + # MongoDB configuration + # MONGODB_URI format: mongodb://host:port/database_name + self.mongo_url = os.getenv("MONGODB_URI", os.getenv("MONGO_URL", "mongodb://localhost:27017")) + + # Determine Mycelia database from environment + # Test environment uses mycelia_test, production uses mycelia + self.mycelia_db = os.getenv("MYCELIA_DB", os.getenv("DATABASE_NAME", "mycelia")) + + # Friend-Lite database - extract from MONGODB_URI or use default + # Test env: test_db, Production: friend-lite + if "/" in self.mongo_url and self.mongo_url.count("/") >= 3: + # Extract database name from mongodb://host:port/database + self.friendlite_db = self.mongo_url.split("/")[-1].split("?")[0] or "friend-lite" + else: + self.friendlite_db = "friend-lite" + + logger.info(f"MyceliaSyncService initialized: {self.mongo_url}, Mycelia DB: {self.mycelia_db}, Friend-Lite DB: {self.friendlite_db}") + + def _hash_api_key_with_salt(self, api_key: str, salt: bytes) -> str: + """Hash API key with salt (matches Mycelia's implementation).""" + h = hashlib.sha256() + h.update(salt) + h.update(api_key.encode('utf-8')) + return base64.b64encode(h.digest()).decode('utf-8') + + def _create_mycelia_api_key( + self, + user_id: str, + user_email: str + ) -> Tuple[str, str]: + """ + Create a Mycelia API key for a Friend-Lite user. + + Args: + user_id: Friend-Lite user ID (MongoDB ObjectId as string) + user_email: User email address + + Returns: + Tuple of (client_id, api_key) + """ + # Generate API key in Mycelia format + random_part = secrets.token_urlsafe(32) + api_key = f"mycelia_{random_part}" + + # Generate salt + salt = secrets.token_bytes(32) + + # Hash the API key + hashed_key = self._hash_api_key_with_salt(api_key, salt) + + # Open prefix for fast lookup + open_prefix = api_key[:16] + + # Connect to Mycelia database + client = MongoClient(self.mongo_url) + db = client[self.mycelia_db] + api_keys_collection = db["api_keys"] + + # Check if user already has an active API key + existing = api_keys_collection.find_one({ + "owner": user_id, + "isActive": True, + "name": f"Friend-Lite Auto ({user_email})" + }) + + if existing: + logger.info(f"User {user_email} already has Mycelia API key: {existing['_id']}") + # Return existing credentials (we can't retrieve the original API key) + # User will need to use the stored credentials + return str(existing["_id"]), None + + # Create new API key document + api_key_doc = { + "hashedKey": hashed_key, + "salt": base64.b64encode(salt).decode('utf-8'), + "owner": user_id, # CRITICAL: owner = Friend-Lite user ID + "name": f"Friend-Lite Auto ({user_email})", + "policies": [ + { + "resource": "**", + "action": "*", + "effect": "allow" + } + ], + "openPrefix": open_prefix, + "createdAt": datetime.utcnow(), + "isActive": True, + } + + # Insert into Mycelia database + result = api_keys_collection.insert_one(api_key_doc) + client_id = str(result.inserted_id) + + logger.info(f"โœ… Created Mycelia API key for {user_email}: {client_id}") + + return client_id, api_key + + def sync_user_to_mycelia( + self, + user_id: str, + user_email: str + ) -> Optional[Tuple[str, str]]: + """ + Sync a Friend-Lite user to Mycelia OAuth. + + Args: + user_id: Friend-Lite user ID + user_email: User email + + Returns: + Tuple of (client_id, api_key) or None if sync fails + """ + try: + # Create Mycelia API key + client_id, api_key = self._create_mycelia_api_key(user_id, user_email) + + # Store credentials in Friend-Lite user document (if new key was created) + if api_key: + client = MongoClient(self.mongo_url) + db = client[self.friendlite_db] + users_collection = db["users"] + + from bson import ObjectId + users_collection.update_one( + {"_id": ObjectId(user_id)}, + { + "$set": { + "mycelia_oauth": { + "client_id": client_id, + "created_at": datetime.utcnow(), + "synced": True + } + } + } + ) + + logger.info(f"โœ… Synced {user_email} with Mycelia OAuth") + return client_id, api_key + else: + logger.info(f"โ„น๏ธ {user_email} already synced with Mycelia") + return client_id, None + + except Exception as e: + logger.error(f"Failed to sync {user_email} to Mycelia: {e}", exc_info=True) + return None + + def sync_admin_user(self) -> Optional[Tuple[str, str]]: + """ + Sync the admin user on startup. + + Returns: + Tuple of (client_id, api_key) if new key created, or None + """ + try: + admin_email = os.getenv("ADMIN_EMAIL") + if not admin_email: + logger.warning("ADMIN_EMAIL not set, skipping Mycelia sync") + return None + + # Get admin user from Friend-Lite database + client = MongoClient(self.mongo_url) + db = client[self.friendlite_db] + users_collection = db["users"] + + admin_user = users_collection.find_one({"email": admin_email}) + if not admin_user: + logger.warning(f"Admin user {admin_email} not found in database") + return None + + user_id = str(admin_user["_id"]) + + # Sync to Mycelia + result = self.sync_user_to_mycelia(user_id, admin_email) + + if result: + client_id, api_key = result + if api_key: + logger.info("="*70) + logger.info("๐Ÿ”‘ MYCELIA OAUTH CREDENTIALS (Save these!)") + logger.info("="*70) + logger.info(f"User: {admin_email}") + logger.info(f"Client ID: {client_id}") + logger.info(f"Client Secret: {api_key}") + logger.info("="*70) + logger.info("Configure Mycelia frontend at http://localhost:3002/settings") + logger.info("="*70) + + return result + + except Exception as e: + logger.error(f"Failed to sync admin user: {e}", exc_info=True) + return None + + +# Global instance +_sync_service: Optional[MyceliaSyncService] = None + + +def get_mycelia_sync_service() -> MyceliaSyncService: + """Get or create the global Mycelia sync service instance.""" + global _sync_service + if _sync_service is None: + _sync_service = MyceliaSyncService() + return _sync_service + + +async def sync_admin_on_startup(): + """Run admin user sync on application startup.""" + logger.info("๐Ÿ”„ Starting Mycelia OAuth synchronization...") + + # Check if Mycelia sync is enabled + memory_provider = os.getenv("MEMORY_PROVIDER", "friend_lite") + if memory_provider != "mycelia": + logger.info("Mycelia sync skipped (MEMORY_PROVIDER != mycelia)") + return + + sync_service = get_mycelia_sync_service() + result = sync_service.sync_admin_user() + + if result: + logger.info("โœ… Mycelia OAuth sync completed") + else: + logger.warning("โš ๏ธ Mycelia OAuth sync completed with warnings") diff --git a/backends/advanced/webui/src/App.tsx b/backends/advanced/webui/src/App.tsx index 39605087..6e497dff 100644 --- a/backends/advanced/webui/src/App.tsx +++ b/backends/advanced/webui/src/App.tsx @@ -5,7 +5,7 @@ import Layout from './components/layout/Layout' import LoginPage from './pages/LoginPage' import Chat from './pages/Chat' import Conversations from './pages/Conversations' -import Memories from './pages/Memories' +import MemoriesRouter from './pages/MemoriesRouter' import Users from './pages/Users' import System from './pages/System' import Upload from './pages/Upload' @@ -51,7 +51,7 @@ function App() { } /> - + } /> { + // Store JWT in localStorage for potential direct Mycelia access + if (token) { + localStorage.setItem('mycelia_jwt_token', token) + } + }, [token]) + + // Always show the native Memories page (works for all providers) + // Friend-Lite backend will proxy to Mycelia when needed + return +} From 37912789cd8dfefdc38a0c318bf36612e29ad5d0 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Mon, 1 Dec 2025 14:26:41 +0000 Subject: [PATCH 03/21] Fixed zombie jobs where a worker could get stuck --- .../advanced_omi_backend/utils/job_utils.py | 44 +++++++++++++++++++ .../workers/audio_jobs.py | 11 +++++ .../workers/conversation_jobs.py | 5 +++ .../workers/transcription_jobs.py | 5 +++ 4 files changed, 65 insertions(+) create mode 100644 backends/advanced/src/advanced_omi_backend/utils/job_utils.py diff --git a/backends/advanced/src/advanced_omi_backend/utils/job_utils.py b/backends/advanced/src/advanced_omi_backend/utils/job_utils.py new file mode 100644 index 00000000..6200af82 --- /dev/null +++ b/backends/advanced/src/advanced_omi_backend/utils/job_utils.py @@ -0,0 +1,44 @@ +""" +Job utility functions for RQ workers. + +This module provides common utilities for long-running RQ jobs. +""" + +import logging +from typing import Optional + +logger = logging.getLogger(__name__) + + +async def check_job_alive(redis_client, current_job) -> bool: + """ + Check if current RQ job still exists in Redis. + + Long-running jobs should call this periodically to detect zombie state + (when the job has been deleted from Redis but the worker is still running). + + Args: + redis_client: Async Redis client + current_job: RQ job instance from get_current_job() + + Returns: + False if job is zombie (caller should exit), True otherwise + + Example: + from rq import get_current_job + from advanced_omi_backend.utils.job_utils import check_job_alive + + current_job = get_current_job() + + while True: + # Check for zombie state each iteration + if not await check_job_alive(redis_client, current_job): + break + # ... do work ... + """ + if current_job: + job_exists = await redis_client.exists(f"rq:job:{current_job.id}") + if not job_exists: + logger.error(f"๐ŸงŸ Zombie job detected - job {current_job.id} deleted from Redis, exiting") + return False + return True diff --git a/backends/advanced/src/advanced_omi_backend/workers/audio_jobs.py b/backends/advanced/src/advanced_omi_backend/workers/audio_jobs.py index 7fc3f323..56df7149 100644 --- a/backends/advanced/src/advanced_omi_backend/workers/audio_jobs.py +++ b/backends/advanced/src/advanced_omi_backend/workers/audio_jobs.py @@ -260,7 +260,18 @@ async def audio_streaming_persistence_job( max_empty_reads = 3 # Exit after 3 consecutive empty reads (deterministic check) conversation_count = 0 + # Get current job for zombie detection + from rq import get_current_job + from advanced_omi_backend.utils.job_utils import check_job_alive + current_job = get_current_job() + while True: + # Check if job still exists in Redis (detect zombie state) + if not await check_job_alive(redis_client, current_job): + if file_sink: + await file_sink.close() + break + # Check timeout if time.time() - start_time > max_runtime: logger.warning(f"โฑ๏ธ Timeout reached for audio persistence {session_id}") diff --git a/backends/advanced/src/advanced_omi_backend/workers/conversation_jobs.py b/backends/advanced/src/advanced_omi_backend/workers/conversation_jobs.py index 8bc6a205..1d4bd985 100644 --- a/backends/advanced/src/advanced_omi_backend/workers/conversation_jobs.py +++ b/backends/advanced/src/advanced_omi_backend/workers/conversation_jobs.py @@ -281,6 +281,11 @@ async def open_conversation_job( logger.info("๐Ÿงช Test mode: Waiting for audio queue to drain before timeout") while True: + # Check if job still exists in Redis (detect zombie state) + from advanced_omi_backend.utils.job_utils import check_job_alive + if not await check_job_alive(redis_client, current_job): + break + # Check if session is finalizing (set by producer when recording stops) if not finalize_received: status = await redis_client.hget(session_key, "status") diff --git a/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py b/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py index 2fc4c5ab..4e340319 100644 --- a/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py +++ b/backends/advanced/src/advanced_omi_backend/workers/transcription_jobs.py @@ -499,6 +499,11 @@ async def stream_speech_detection_job( # Main loop: Listen for speech while True: + # Check if job still exists in Redis (detect zombie state) + from advanced_omi_backend.utils.job_utils import check_job_alive + if not await check_job_alive(redis_client, current_job): + break + # Exit conditions session_status = await redis_client.hget(session_key, "status") if session_status and session_status.decode() in ["complete", "closed"]: From eb4df567768f5c6c471dadf8b9520479290fd4c6 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Mon, 1 Dec 2025 16:14:22 +0000 Subject: [PATCH 04/21] fixed delete for mycelia with needing user_id --- .../advanced_omi_backend/controllers/memory_controller.py | 8 +++++--- .../src/advanced_omi_backend/services/memory/base.py | 8 +++++--- .../services/memory/providers/__init__.py | 3 --- .../services/memory/providers/friend_lite.py | 2 +- .../services/memory/providers/mcp_client.py | 4 ++-- .../services/memory/providers/openmemory_mcp.py | 2 +- .../services/memory/providers/vector_stores.py | 4 ++-- extras/mycelia | 2 +- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py index d917ec18..220ba815 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py @@ -117,12 +117,14 @@ async def delete_memory(memory_id: str, user: User): # Check if memory belongs to current user user_memories = await memory_service.get_all_memories(user.user_id, 1000) - memory_ids = [str(mem.get("id", mem.get("memory_id", ""))) for mem in user_memories] + # MemoryEntry is a dataclass, access id attribute directly + memory_ids = [str(mem.id) for mem in user_memories] if memory_id not in memory_ids: return JSONResponse(status_code=404, content={"message": "Memory not found"}) - # Delete the memory - success = await memory_service.delete_memory(memory_id) + # Delete the memory (pass user_id and user_email for Mycelia authentication) + audio_logger.info(f"Deleting memory {memory_id} for user_id={user.user_id}, email={user.email}") + success = await memory_service.delete_memory(memory_id, user_id=user.user_id, user_email=user.email) if success: return JSONResponse(content={"message": f"Memory {memory_id} deleted successfully"}) diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/base.py b/backends/advanced/src/advanced_omi_backend/services/memory/base.py index f205ecdb..f557c9af 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/base.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/base.py @@ -163,12 +163,14 @@ async def count_memories(self, user_id: str) -> Optional[int]: return None @abstractmethod - async def delete_memory(self, memory_id: str) -> bool: + async def delete_memory(self, memory_id: str, user_id: Optional[str] = None, user_email: Optional[str] = None) -> bool: """Delete a specific memory by ID. - + Args: memory_id: Unique identifier of the memory to delete - + user_id: Optional user ID for authentication (required for Mycelia provider) + user_email: Optional user email for authentication (required for Mycelia provider) + Returns: True if successfully deleted, False otherwise """ diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/__init__.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/__init__.py index 591fbc2b..43d438cf 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/__init__.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/__init__.py @@ -7,7 +7,6 @@ - llm_providers: LLM provider implementations (OpenAI, Ollama) - vector_stores: Vector store implementations (Qdrant) - mcp_client: MCP client utilities -- compat_service: Backward compatibility wrapper """ from .friend_lite import MemoryService as FriendLiteMemoryService @@ -16,7 +15,6 @@ from .llm_providers import OpenAIProvider from .vector_stores import QdrantVectorStore from .mcp_client import MCPClient, MCPError -from .compat_service import MemoryService __all__ = [ "FriendLiteMemoryService", @@ -26,5 +24,4 @@ "QdrantVectorStore", "MCPClient", "MCPError", - "MemoryService", ] diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py index be91a5f5..b3909a65 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py @@ -291,7 +291,7 @@ async def count_memories(self, user_id: str) -> Optional[int]: memory_logger.error(f"Count memories failed: {e}") return None - async def delete_memory(self, memory_id: str) -> bool: + async def delete_memory(self, memory_id: str, user_id: Optional[str] = None, user_email: Optional[str] = None) -> bool: """Delete a specific memory by ID. Args: diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py index 7942a17a..a1b9876f 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py @@ -6,7 +6,7 @@ import logging import uuid -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional import httpx memory_logger = logging.getLogger("memory_service") @@ -339,7 +339,7 @@ async def delete_all_memories(self) -> int: memory_logger.error(f"Error deleting all memories: {e}") return 0 - async def delete_memory(self, memory_id: str) -> bool: + async def delete_memory(self, memory_id: str, user_id: Optional[str] = None, user_email: Optional[str] = None) -> bool: """Delete a specific memory by ID. Args: diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py index d5f8acd9..04b8fd67 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py @@ -283,7 +283,7 @@ async def get_all_memories( # Restore original user_id self.mcp_client.user_id = original_user_id - async def delete_memory(self, memory_id: str) -> bool: + async def delete_memory(self, memory_id: str, user_id: Optional[str] = None, user_email: Optional[str] = None) -> bool: """Delete a specific memory by ID. Args: diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/vector_stores.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/vector_stores.py index a3d04100..cf153472 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/vector_stores.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/vector_stores.py @@ -9,7 +9,7 @@ import logging import time import uuid -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from qdrant_client import AsyncQdrantClient from qdrant_client.models import ( @@ -240,7 +240,7 @@ async def get_memories(self, user_id: str, limit: int) -> List[MemoryEntry]: memory_logger.error(f"Qdrant get memories failed: {e}") return [] - async def delete_memory(self, memory_id: str) -> bool: + async def delete_memory(self, memory_id: str, user_id: Optional[str] = None, user_email: Optional[str] = None) -> bool: """Delete a specific memory from Qdrant.""" try: # Convert memory_id to proper format for Qdrant diff --git a/extras/mycelia b/extras/mycelia index ca7b177b..6c27e2cc 160000 --- a/extras/mycelia +++ b/extras/mycelia @@ -1 +1 @@ -Subproject commit ca7b177b1e9228b63399da557a1ddbf696cf6762 +Subproject commit 6c27e2ccafd6d22933d35b5399f62552097a36b3 From 224982e30bd6a3765ae81e16dcf39540b8c41adb Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Mon, 1 Dec 2025 17:42:03 +0000 Subject: [PATCH 05/21] removed friend or would be cicular --- extras/mycelia | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/mycelia b/extras/mycelia index 6c27e2cc..47ea1966 160000 --- a/extras/mycelia +++ b/extras/mycelia @@ -1 +1 @@ -Subproject commit 6c27e2ccafd6d22933d35b5399f62552097a36b3 +Subproject commit 47ea1966dd8a8c10662c91c7a3f907798f6a7dbc From 8e5612ce83f8f6428e7e5145caa01b87c3ca7f41 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Mon, 1 Dec 2025 17:59:36 +0000 Subject: [PATCH 06/21] added temporal memopries --- .../services/memory/__init__.py | 139 +------------- .../services/memory/config.py | 18 +- .../services/memory/prompts.py | 171 +++++++++++++++++- .../services/memory/service_factory.py | 3 +- 4 files changed, 198 insertions(+), 133 deletions(-) diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/__init__.py b/backends/advanced/src/advanced_omi_backend/services/memory/__init__.py index 42cba194..c2413ff2 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/__init__.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/__init__.py @@ -1,151 +1,30 @@ """Memory service package. This package provides memory management functionality with support for -multiple LLM providers and vector stores for the Omi backend. +multiple memory providers (Friend-Lite, Mycelia, OpenMemory MCP). The memory service handles extraction, storage, and retrieval of memories from user conversations and interactions. Architecture: - base.py: Abstract base classes and interfaces -- memory_service.py: Core implementation -- compat_service.py: Backward compatibility wrapper -- providers/: LLM and vector store implementations - config.py: Configuration management +- service_factory.py: Provider selection and instantiation +- providers/friend_lite.py: Friend-Lite native provider (LLM + Qdrant) +- providers/mycelia.py: Mycelia backend provider +- providers/openmemory_mcp.py: OpenMemory MCP provider +- providers/llm_providers.py: LLM implementations (OpenAI, Ollama) +- providers/vector_stores.py: Vector store implementations (Qdrant) """ import logging memory_logger = logging.getLogger("memory_service") -# Initialize core functions to None -get_memory_service = None -MemoryService = None -shutdown_memory_service = None -test_new_memory_service = None -migrate_from_mem0 = None - -memory_logger.info("๐Ÿ†• Using NEW memory service implementation") -try: - from .providers.compat_service import ( - MemoryService, - get_memory_service, - migrate_from_mem0, - shutdown_memory_service, - ) - - # Also import core implementation for direct access - from .providers.friend_lite import MemoryService as CoreMemoryService - test_new_memory_service = None # Will be implemented if needed -except ImportError as e: - memory_logger.error(f"Failed to import new memory service: {e}") - raise - -# Also export the new architecture components for direct access when needed -try: - from .base import LLMProviderBase, MemoryEntry, MemoryServiceBase, VectorStoreBase - from .config import MemoryProvider # New memory provider enum - from .config import create_openmemory_config # New OpenMemory config function - from .config import ( - LLMProvider, - MemoryConfig, - VectorStoreProvider, - build_memory_config_from_env, - create_ollama_config, - create_openai_config, - create_qdrant_config, - ) - from .providers.openmemory_mcp import OpenMemoryMCPService # New complete memory service - from .providers.mcp_client import MCPClient, MCPError - from .providers.llm_providers import OpenAIProvider - from .providers.vector_stores import QdrantVectorStore - from .service_factory import create_memory_service - from .service_factory import get_memory_service as get_core_memory_service - from .service_factory import get_service_info as get_core_service_info - from .service_factory import reset_memory_service - from .service_factory import shutdown_memory_service as shutdown_core_memory_service - - # Keep backward compatibility alias - AbstractMemoryService = CoreMemoryService -except ImportError as e: - memory_logger.warning(f"Some advanced memory service components not available: {e}") - MemoryServiceBase = None - LLMProviderBase = None - VectorStoreBase = None - AbstractMemoryService = None - MemoryConfig = None - LLMProvider = None - VectorStoreProvider = None - MemoryProvider = None - build_memory_config_from_env = None - create_openai_config = None - create_ollama_config = None - create_qdrant_config = None - create_openmemory_config = None - MemoryEntry = None - OpenAIProvider = None - QdrantVectorStore = None - OpenMemoryMCPService = None - MCPClient = None - MCPError = None - get_core_memory_service = None - create_memory_service = None - shutdown_core_memory_service = None - reset_memory_service = None - get_core_service_info = None +# Import the main interface functions from service_factory +from .service_factory import get_memory_service, shutdown_memory_service __all__ = [ - # Main interface (compatible with legacy) "get_memory_service", - "MemoryService", "shutdown_memory_service", - - # New service specific (may be None if not available) - "test_new_memory_service", - "migrate_from_mem0", - "CoreMemoryService", - - # Base classes (new architecture) - "MemoryServiceBase", - "LLMProviderBase", - "VectorStoreBase", - - # Advanced components (may be None if not available) - "AbstractMemoryService", # Backward compatibility alias - "MemoryConfig", - "MemoryEntry", - "LLMProvider", - "VectorStoreProvider", - "MemoryProvider", # New enum - "build_memory_config_from_env", - "create_openai_config", - "create_ollama_config", - "create_qdrant_config", - "create_openmemory_config", # New function - "OpenAIProvider", - "QdrantVectorStore", - - # Complete memory service implementations - "OpenMemoryMCPService", - - # MCP client components - "MCPClient", - "MCPError", - - # Service factory functions - "get_core_memory_service", - "create_memory_service", - "shutdown_core_memory_service", - "reset_memory_service", - "get_core_service_info" ] - -def get_service_info(): - """Get information about which service is currently active.""" - return { - "active_service": "new", # Always use new service - "new_service_available": CoreMemoryService is not None, - "legacy_service_available": True, # Assume always available - "base_classes_available": MemoryServiceBase is not None, - "core_service_available": CoreMemoryService is not None - } \ No newline at end of file diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/config.py b/backends/advanced/src/advanced_omi_backend/services/memory/config.py index 9d5c8324..3946deae 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/config.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/config.py @@ -168,18 +168,34 @@ def build_memory_config_from_env() -> MemoryConfig: timeout_seconds=int(os.getenv("OPENMEMORY_TIMEOUT", "30")) ) - # For Mycelia provider, configuration is simple - just URL + # For Mycelia provider, build mycelia_config + llm_config (for temporal extraction) if memory_provider_enum == MemoryProvider.MYCELIA: mycelia_config = create_mycelia_config( api_url=os.getenv("MYCELIA_URL", "http://localhost:5173"), timeout=int(os.getenv("MYCELIA_TIMEOUT", "30")) ) + # Build LLM config for temporal extraction (Mycelia provider uses OpenAI directly) + openai_api_key = os.getenv("OPENAI_API_KEY") + if not openai_api_key: + memory_logger.warning("OPENAI_API_KEY not set - temporal extraction will be disabled") + llm_config = None + else: + model = os.getenv("OPENAI_MODEL", "gpt-4o-mini") + base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") + llm_config = create_openai_config( + api_key=openai_api_key, + model=model, + base_url=base_url + ) + memory_logger.info(f"๐Ÿ”ง Mycelia temporal extraction: LLM={model}") + memory_logger.info(f"๐Ÿ”ง Memory config: Provider=Mycelia, URL={mycelia_config['api_url']}") return MemoryConfig( memory_provider=memory_provider_enum, mycelia_config=mycelia_config, + llm_config=llm_config, timeout_seconds=int(os.getenv("MYCELIA_TIMEOUT", "30")) ) diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/prompts.py b/backends/advanced/src/advanced_omi_backend/services/memory/prompts.py index f655752e..b022e39c 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/prompts.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/prompts.py @@ -5,10 +5,13 @@ 2. Updating memory with new facts (DEFAULT_UPDATE_MEMORY_PROMPT) 3. Answering questions from memory (MEMORY_ANSWER_PROMPT) 4. Procedural memory for task tracking (PROCEDURAL_MEMORY_SYSTEM_PROMPT) +5. Temporal and entity extraction (TEMPORAL_ENTITY_EXTRACTION_PROMPT) """ -from datetime import datetime +from datetime import datetime, timedelta import json +from typing import List, Optional +from pydantic import BaseModel, Field MEMORY_ANSWER_PROMPT = """ You are an expert at answering questions based on the provided memories. Your task is to provide accurate and concise answers to the questions by leveraging the information given in the memories. @@ -383,3 +386,169 @@ def get_update_memory_messages(retrieved_old_memory_dict, response_content, cust Do not return anything except the JSON format. """ + + +# ===== Temporal and Entity Extraction ===== + +class TimeRange(BaseModel): + """Represents a time range with start and end timestamps.""" + start: datetime = Field(description="ISO 8601 timestamp when the event/activity starts") + end: datetime = Field(description="ISO 8601 timestamp when the event/activity ends") + name: Optional[str] = Field(default=None, description="Optional name/label for this time range (e.g., 'wedding ceremony', 'party')") + + +class TemporalEntity(BaseModel): + """Structured temporal and entity information extracted from a memory fact.""" + isEvent: bool = Field(description="Whether this memory describes a scheduled event or time-bound activity") + isPerson: bool = Field(description="Whether this memory is primarily about a person or people") + isPlace: bool = Field(description="Whether this memory is primarily about a location or place") + isPromise: bool = Field(description="Whether this memory contains a commitment, promise, or agreement") + isRelationship: bool = Field(description="Whether this memory describes a relationship between people") + entities: List[str] = Field(default_factory=list, description="List of people, places, or things mentioned (e.g., ['John', 'Botanical Gardens', 'wedding'])") + timeRanges: List[TimeRange] = Field(default_factory=list, description="List of time ranges if this is a temporal memory") + emoji: Optional[str] = Field(default=None, description="Single emoji that best represents this memory") + + +def build_temporal_extraction_prompt(current_date: datetime) -> str: + """Build the temporal extraction prompt with the current date context.""" + return f"""You are an expert at extracting temporal and entity information from memory facts. + +Your task is to analyze a memory fact and extract structured information in JSON format: +1. **Entity Types**: Determine if the memory is about events, people, places, promises, or relationships +2. **Temporal Information**: Extract and resolve any time references to actual ISO 8601 timestamps +3. **Named Entities**: List all people, places, and things mentioned +4. **Representation**: Choose a single emoji that captures the essence of the memory + +You must return a valid JSON object with the following structure. + +**Current Date Context:** +- Today's date: {current_date.strftime("%Y-%m-%d")} +- Current time: {current_date.strftime("%H:%M:%S")} +- Day of week: {current_date.strftime("%A")} + +**Time Resolution Guidelines:** + +Relative Time References: +- "tomorrow" โ†’ Add 1 day to current date +- "next week" โ†’ Add 7 days to current date +- "in X days/weeks/months" โ†’ Add X time units to current date +- "yesterday" โ†’ Subtract 1 day from current date + +Time of Day: +- "4pm" or "16:00" โ†’ Use current date with that time +- "tomorrow at 4pm" โ†’ Use tomorrow's date at 16:00 +- "morning" โ†’ 09:00 on the referenced day +- "afternoon" โ†’ 14:00 on the referenced day +- "evening" โ†’ 18:00 on the referenced day +- "night" โ†’ 21:00 on the referenced day + +Duration Estimation (when only start time is mentioned): +- Events like "wedding", "meeting", "party" โ†’ Default 2 hours duration +- "lunch", "dinner", "breakfast" โ†’ Default 1 hour duration +- "class", "workshop" โ†’ Default 1.5 hours duration +- "appointment", "call" โ†’ Default 30 minutes duration + +**Entity Type Guidelines:** + +- **isEvent**: True for scheduled activities, appointments, meetings, parties, ceremonies, classes, etc. +- **isPerson**: True when the primary focus is on a person (e.g., "Met John", "Sarah is my friend") +- **isPlace**: True when the primary focus is a location (e.g., "Botanical Gardens is beautiful", "Favorite restaurant is...") +- **isPromise**: True for commitments, promises, or agreements (e.g., "I'll call you tomorrow", "We agreed to meet") +- **isRelationship**: True for statements about relationships (e.g., "John is my brother", "We're getting married") + +**Examples:** + +Input: "I'm getting married in one week! It's going to be at 4pm at the botanical gardens." +Output: +{{ + "isEvent": true, + "isPerson": false, + "isPlace": false, + "isPromise": false, + "isRelationship": true, + "entities": ["botanical gardens", "wedding"], + "timeRanges": [ + {{ + "start": "{(current_date.replace(hour=16, minute=0, second=0) + timedelta(days=7)).isoformat()}", + "end": "{(current_date.replace(hour=18, minute=0, second=0) + timedelta(days=7)).isoformat()}", + "name": "wedding ceremony" + }} + ], + "emoji": "๐Ÿ’’" +}} + +Input: "Had a meeting with John at 3pm to discuss the new project" +Output: +{{ + "isEvent": true, + "isPerson": true, + "isPlace": false, + "isPromise": false, + "isRelationship": false, + "entities": ["John", "new project", "meeting"], + "timeRanges": [ + {{ + "start": "{current_date.replace(hour=15, minute=0, second=0).isoformat()}", + "end": "{current_date.replace(hour=16, minute=0, second=0).isoformat()}", + "name": "meeting" + }} + ], + "emoji": "๐Ÿค" +}} + +Input: "My favorite restaurant is Giovanni's Italian Kitchen" +Output: +{{ + "isEvent": false, + "isPerson": false, + "isPlace": true, + "isPromise": false, + "isRelationship": false, + "entities": ["Giovanni's Italian Kitchen", "restaurant"], + "timeRanges": [], + "emoji": "๐Ÿ" +}} + +Input: "I love hiking in the mountains" +Output: +{{ + "isEvent": false, + "isPerson": false, + "isPlace": false, + "isPromise": false, + "isRelationship": false, + "entities": ["mountains", "hiking"], + "timeRanges": [], + "emoji": "๐Ÿ”๏ธ" +}} + +Input: "Tomorrow I need to call Sarah about the party at 2pm" +Output: +{{ + "isEvent": true, + "isPerson": true, + "isPlace": false, + "isPromise": true, + "isRelationship": false, + "entities": ["Sarah", "party", "call"], + "timeRanges": [ + {{ + "start": "{(current_date.replace(hour=14, minute=0, second=0) + timedelta(days=1)).isoformat()}", + "end": "{(current_date.replace(hour=14, minute=30, second=0) + timedelta(days=1)).isoformat()}", + "name": "call Sarah" + }} + ], + "emoji": "๐Ÿ“ž" +}} + +**Instructions:** +- Return structured data following the TemporalEntity schema +- Convert all temporal references to ISO 8601 format +- Be conservative: if there's no temporal information, leave timeRanges empty +- Multiple tags can be true (e.g., isEvent and isPerson both true for "meeting with John") +- Extract all meaningful entities (people, places, things) mentioned in the fact +- Choose an emoji that best represents the core meaning of the memory +""" + + +TEMPORAL_ENTITY_EXTRACTION_PROMPT = build_temporal_extraction_prompt(datetime.now()) diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/service_factory.py b/backends/advanced/src/advanced_omi_backend/services/memory/service_factory.py index a51f4edc..37922186 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/service_factory.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/service_factory.py @@ -63,7 +63,8 @@ def create_memory_service(config: MemoryConfig) -> MemoryServiceBase: if not config.mycelia_config: raise ValueError("Mycelia configuration is required for MYCELIA provider") - return MyceliaMemoryService(**config.mycelia_config) + # Pass the full config so Mycelia can access llm_config + return MyceliaMemoryService(config) else: raise ValueError(f"Unsupported memory provider: {config.memory_provider}") From 11b856e84f63eaeff0e9defbca9b7f0f77d76f6e Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Wed, 3 Dec 2025 19:57:21 +0000 Subject: [PATCH 07/21] added methods to memory service baseclass Tweaks to get all memory providers working --- .../services/memory/base.py | 70 ++- .../memory/providers/compat_service.py | 460 ------------------ .../services/memory/providers/friend_lite.py | 5 +- .../services/memory/providers/mcp_client.py | 110 ++++- .../services/memory/providers/mycelia.py | 452 +++++++++++++++-- .../memory/providers/openmemory_mcp.py | 142 ++++-- .../workers/memory_jobs.py | 4 +- 7 files changed, 690 insertions(+), 553 deletions(-) delete mode 100644 backends/advanced/src/advanced_omi_backend/services/memory/providers/compat_service.py diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/base.py b/backends/advanced/src/advanced_omi_backend/services/memory/base.py index f557c9af..e88e42d4 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/base.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/base.py @@ -150,18 +150,59 @@ async def get_all_memories( async def count_memories(self, user_id: str) -> Optional[int]: """Count total number of memories for a user. - + This is an optional method that providers can implement for efficient counting. Returns None if the provider doesn't support counting. - + Args: user_id: User identifier - + Returns: Total count of memories for the user, or None if not supported """ return None - + + async def get_memory(self, memory_id: str, user_id: Optional[str] = None) -> Optional[MemoryEntry]: + """Get a specific memory by ID. + + This is an optional method that providers can implement for fetching + individual memories. Returns None if the provider doesn't support it + or the memory is not found. + + Args: + memory_id: Unique identifier of the memory to retrieve + user_id: Optional user ID for authentication/filtering + + Returns: + MemoryEntry object if found, None otherwise + """ + return None + + async def update_memory( + self, + memory_id: str, + content: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + user_id: Optional[str] = None, + user_email: Optional[str] = None + ) -> bool: + """Update a specific memory's content and/or metadata. + + This is an optional method that providers can implement for updating + existing memories. Returns False if not supported or update fails. + + Args: + memory_id: Unique identifier of the memory to update + content: New content for the memory (if None, content is not updated) + metadata: New metadata to merge with existing (if None, metadata is not updated) + user_id: Optional user ID for authentication + user_email: Optional user email for authentication + + Returns: + True if update succeeded, False otherwise + """ + return False + @abstractmethod async def delete_memory(self, memory_id: str, user_id: Optional[str] = None, user_email: Optional[str] = None) -> bool: """Delete a specific memory by ID. @@ -205,6 +246,27 @@ def shutdown(self) -> None: """ pass + def __init__(self): + """Initialize base memory service state. + + Subclasses should call super().__init__() in their constructors. + """ + self._initialized = False + + async def _ensure_initialized(self) -> None: + """Ensure the memory service is initialized before use. + + This method provides lazy initialization - it will automatically + call initialize() the first time it's needed. This is critical + for services used in RQ workers where the service instance is + created in one process but used in another. + + This should be called at the start of any method that requires + the service to be initialized (e.g., add_memory, search_memories). + """ + if not self._initialized: + await self.initialize() + class LLMProviderBase(ABC): """Abstract base class for LLM provider implementations. diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/compat_service.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/compat_service.py deleted file mode 100644 index 361f8bcd..00000000 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/compat_service.py +++ /dev/null @@ -1,460 +0,0 @@ -"""Compatibility service for backward compatibility. - -This module provides a drop-in replacement for the original mem0-based -memory service, maintaining the same interface while using the new -architecture internally. -""" - -import json -import logging -import os -from typing import Any, Dict, List, Optional, Tuple - -from ..config import build_memory_config_from_env -from .friend_lite import MemoryService as CoreMemoryService - -memory_logger = logging.getLogger("memory_service") - - -class MemoryService: - """Drop-in replacement for the original mem0-based MemoryService. - - This class provides backward compatibility by wrapping the new - CoreMemoryService with the same interface as the original service. - It handles data format conversion and maintains compatibility with - existing code. - - Attributes: - _service: Internal CoreMemoryService instance - _initialized: Whether the service has been initialized - """ - - def __init__(self): - """Initialize the compatibility memory service.""" - self._service: Optional[CoreMemoryService] = None - self._initialized = False - - async def initialize(self): - """Initialize the memory service. - - Raises: - RuntimeError: If initialization fails - """ - if self._initialized: - return - - try: - config = build_memory_config_from_env() - self._service = CoreMemoryService(config) - await self._service.initialize() - self._initialized = True - memory_logger.info("โœ… Memory service initialized successfully") - except Exception as e: - memory_logger.error(f"Failed to initialize memory service: {e}") - raise - - async def add_memory( - self, - transcript: str, - client_id: str, - source_id: str, - user_id: str, - user_email: str, - allow_update: bool = False, - db_helper=None, - ) -> Tuple[bool, List[str]]: - """Add memory from transcript - compatible with original interface. - - Args: - transcript: Raw transcript text to extract memories from - client_id: Client identifier - source_id: Unique identifier for the source (audio session, chat session, etc.) - user_id: User identifier - user_email: User email address - allow_update: Whether to allow updating existing memories - db_helper: Optional database helper for tracking relationships - - Returns: - Tuple of (success: bool, created_memory_ids: List[str]) - """ - if not self._initialized: - await self.initialize() - - # Ensure service is initialized if it's not the internal CoreMemoryService - if hasattr(self._service, 'initialize') and hasattr(self._service, '_initialized'): - if not self._service._initialized: - await self._service.initialize() - - return await self._service.add_memory( - transcript=transcript, - client_id=client_id, - source_id=source_id, - user_id=user_id, - user_email=user_email, - allow_update=allow_update, - db_helper=db_helper - ) - - def _normalize_memory_content(self, content: str, metadata: Dict[str, Any]) -> str: - """Return memory content as-is since individual facts are now stored separately. - - Args: - content: Memory content from the provider - metadata: Memory metadata (not used) - - Returns: - Content as-is (no normalization needed) - """ - return content - - async def get_all_memories(self, user_id: str, limit: int = 100) -> List[Dict[str, Any]]: - """Get all memories for a user - returns dict format for compatibility. - - Args: - user_id: User identifier - limit: Maximum number of memories to return - - Returns: - List of memory dictionaries in legacy format - """ - if not self._initialized: - await self.initialize() - - memories = await self._service.get_all_memories(user_id, limit) - - # Convert MemoryEntry objects to dict format for compatibility with normalized content - return [ - { - "id": memory.id, - "memory": self._normalize_memory_content(memory.content, memory.metadata), - "metadata": memory.metadata, - "created_at": memory.created_at, - "score": memory.score - } - for memory in memories - ] - - async def count_memories(self, user_id: str) -> Optional[int]: - """Count total number of memories for a user. - - Args: - user_id: User identifier - - Returns: - Total count of memories for the user, or None if not supported - """ - if not self._initialized: - await self.initialize() - - # Delegate to the core service - return await self._service.count_memories(user_id) - - async def get_all_memories_unfiltered(self, user_id: str, limit: int = 100) -> List[Dict[str, Any]]: - """Get all memories without filtering - same as get_all_memories in new implementation. - - Args: - user_id: User identifier - limit: Maximum number of memories to return - - Returns: - List of memory dictionaries in legacy format - """ - return await self.get_all_memories(user_id, limit) - - async def search_memories(self, query: str, user_id: str, limit: int = 10, score_threshold: float = 0.0) -> List[Dict[str, Any]]: - """Search memories using semantic similarity - returns dict format for compatibility. - - Args: - query: Search query text - user_id: User identifier to filter memories - limit: Maximum number of results to return - score_threshold: Minimum similarity score (0.0 = no threshold) - - Returns: - List of memory dictionaries in legacy format ordered by relevance - """ - if not self._initialized: - await self.initialize() - - memories = await self._service.search_memories(query, user_id, limit, score_threshold) - - # Convert MemoryEntry objects to dict format for compatibility with normalized content - return [ - { - "id": memory.id, - "memory": self._normalize_memory_content(memory.content, memory.metadata), - "metadata": memory.metadata, - "created_at": memory.created_at, - "score": memory.score - } - for memory in memories - ] - - async def delete_all_user_memories(self, user_id: str) -> int: - """Delete all memories for a user and return count. - - Args: - user_id: User identifier - - Returns: - Number of memories that were deleted - """ - if not self._initialized: - await self.initialize() - - return await self._service.delete_all_user_memories(user_id) - - async def delete_memory(self, memory_id: str) -> bool: - """Delete a specific memory by ID. - - Args: - memory_id: Unique identifier of the memory to delete - - Returns: - True if successfully deleted, False otherwise - """ - if not self._initialized: - await self.initialize() - - return await self._service.delete_memory(memory_id) - - async def get_all_memories_debug(self, limit: int = 200) -> List[Dict[str, Any]]: - """Get all memories across all users for admin debugging. - - Args: - limit: Maximum number of memories to return - - Returns: - List of memory dictionaries with user context for debugging - """ - if not self._initialized: - await self.initialize() - - # Import User model to get all users - try: - from advanced_omi_backend.users import User - except ImportError: - memory_logger.error("Cannot import User model for debug function") - return [] - - all_memories = [] - users = await User.find_all().to_list() - - for user in users: - user_id = str(user.id) - try: - user_memories = await self.get_all_memories(user_id) - - # Add user context for debugging - for memory in user_memories: - memory_entry = { - **memory, - "user_id": user_id, - "owner_email": user.email, - "collection": "omi_memories" - } - all_memories.append(memory_entry) - - # Respect limit - if len(all_memories) >= limit: - break - - except Exception as e: - memory_logger.warning(f"Error getting memories for user {user_id}: {e}") - continue - - return all_memories[:limit] - - async def get_memories_with_transcripts(self, user_id: str, limit: int = 100) -> List[Dict[str, Any]]: - """Get memories with their source transcripts using database relationship. - - Args: - user_id: User identifier - limit: Maximum number of memories to return - - Returns: - List of enriched memory dictionaries with transcript information - """ - if not self._initialized: - await self.initialize() - - # Get memories first - memories = await self.get_all_memories(user_id, limit) - - # Import Conversation model - try: - from advanced_omi_backend.models.conversation import Conversation - except ImportError: - memory_logger.error("Cannot import Conversation model") - return memories # Return memories without transcript enrichment - - # Extract source IDs for bulk query - source_ids = [] - for memory in memories: - metadata = memory.get("metadata", {}) - source_id = metadata.get("source_id") or metadata.get("audio_uuid") # Backward compatibility - if source_id: - source_ids.append(source_id) - - # Bulk query for conversations (support both old audio_uuid and new source_id) - conversations_list = await Conversation.find( - Conversation.audio_uuid.in_(source_ids) - ).to_list() - - conversations_by_id = {} - for conv in conversations_list: - conversations_by_id[conv.audio_uuid] = conv - - enriched_memories = [] - - for memory in memories: - enriched_memory = { - "memory_id": memory.get("id", "unknown"), - "memory_text": memory.get("memory", ""), - "created_at": memory.get("created_at", ""), - "metadata": memory.get("metadata", {}), - "source_id": None, - "transcript": None, - "client_id": None, - "user_email": None, - "compression_ratio": 0, - "transcript_length": 0, - "memory_length": 0, - } - - # Extract source_id from memory metadata (with backward compatibility) - metadata = memory.get("metadata", {}) - source_id = metadata.get("source_id") or metadata.get("audio_uuid") - - if source_id: - enriched_memory["source_id"] = source_id - enriched_memory["client_id"] = metadata.get("client_id") - enriched_memory["user_email"] = metadata.get("user_email") - - # Get transcript from bulk-loaded conversations - conversation = conversations_by_id.get(source_id) - if conversation: - transcript_segments = conversation.segments - if transcript_segments: - full_transcript = " ".join( - segment.text - for segment in transcript_segments - if segment.text - ) - - if full_transcript.strip(): - enriched_memory["transcript"] = full_transcript - enriched_memory["transcript_length"] = len(full_transcript) - - memory_text = enriched_memory["memory_text"] - enriched_memory["memory_length"] = len(memory_text) - - # Calculate compression ratio - if len(full_transcript) > 0: - enriched_memory["compression_ratio"] = round( - (len(memory_text) / len(full_transcript)) * 100, 1 - ) - - enriched_memories.append(enriched_memory) - - return enriched_memories - - async def test_connection(self) -> bool: - """Test memory service connection. - - Returns: - True if connection successful, False otherwise - """ - try: - if not self._initialized: - await self.initialize() - return await self._service.test_connection() - except Exception as e: - memory_logger.error(f"Connection test failed: {e}") - return False - - def shutdown(self): - """Shutdown the memory service and clean up resources.""" - if self._service: - self._service.shutdown() - self._initialized = False - self._service = None - memory_logger.info("Memory service shut down") - - -# Global service instance - maintains compatibility with original code -_memory_service = None - - -def get_memory_service() -> MemoryService: - """Get the global memory service instance. - - Returns: - Global MemoryService instance (singleton pattern), wrapped for compatibility - """ - global _memory_service - if _memory_service is None: - # Use the new service factory to create the appropriate service - from ..service_factory import get_memory_service as get_core_service - - core_service = get_core_service() - - # If it's already a compat service, use it directly - if isinstance(core_service, MemoryService): - _memory_service = core_service - else: - # Wrap core service with compat layer - _memory_service = MemoryService() - _memory_service._service = core_service - _memory_service._initialized = True - - return _memory_service - - -def shutdown_memory_service(): - """Shutdown the global memory service and clean up resources.""" - global _memory_service - if _memory_service: - _memory_service.shutdown() - _memory_service = None - - # Also shutdown the core service - from .service_factory import shutdown_memory_service as shutdown_core_service - shutdown_core_service() - - -# Migration helper functions -async def migrate_from_mem0(): - """Helper function to migrate existing mem0 data to new format. - - This is a placeholder for migration logic. Actual implementation - would depend on the specific mem0 setup and data format. - - Raises: - RuntimeError: If migration fails - """ - memory_logger.info("๐Ÿ”„ Starting migration from mem0 to new memory service") - - try: - # Initialize new memory service - new_service = get_memory_service() - await new_service.initialize() - - # Get all users - try: - from advanced_omi_backend.users import User - users = await User.find_all().to_list() - except ImportError: - memory_logger.error("Cannot import User model for migration") - return - - # Migration steps would go here: - # 1. For each user, get their mem0 memories (if accessible) - # 2. Convert to new format - # 3. Store in new system - - memory_logger.info("โœ… Migration completed successfully") - - except Exception as e: - memory_logger.error(f"โŒ Migration failed: {e}") - raise \ No newline at end of file diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py index b3909a65..a0974e21 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/friend_lite.py @@ -45,10 +45,10 @@ def __init__(self, config: MemoryConfig): Args: config: MemoryConfig instance with provider settings """ + super().__init__() self.config = config self.llm_provider: Optional[LLMProviderBase] = None self.vector_store: Optional[VectorStoreBase] = None - self._initialized = False async def initialize(self) -> None: """Initialize the memory service and all its components. @@ -129,8 +129,7 @@ async def add_memory( Raises: asyncio.TimeoutError: If processing exceeds timeout """ - if not self._initialized: - await self.initialize() + await self._ensure_initialized() try: # Skip empty transcripts diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py index a1b9876f..15226971 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py @@ -28,18 +28,20 @@ class MCPClient: client: HTTP client instance """ - def __init__(self, server_url: str, client_name: str = "friend_lite", user_id: str = "default", timeout: int = 30): + def __init__(self, server_url: str, client_name: str = "friend_lite", user_id: str = "default", user_email: str = "", timeout: int = 30): """Initialize client for OpenMemory. - + Args: server_url: Base URL of the OpenMemory server client_name: Client identifier (used as app name) user_id: User identifier for memory isolation + user_email: User email address for user metadata timeout: HTTP request timeout in seconds """ self.server_url = server_url.rstrip('/') self.client_name = client_name self.user_id = user_id + self.user_email = user_email self.timeout = timeout # Use custom CA certificate if available @@ -107,18 +109,20 @@ async def add_memories(self, text: str) -> List[str]: memory_logger.error("No apps found in OpenMemory - cannot create memory") raise MCPError("No apps found in OpenMemory") - # Use REST API endpoint for creating memories (trailing slash required) + # Use REST API endpoint for creating memories + # The 'app' field can be either app name (string) or app UUID response = await self.client.post( f"{self.server_url}/api/v1/memories/", json={ "user_id": self.user_id, "text": text, + "app": self.client_name, # Use app name (OpenMemory accepts name or UUID) "metadata": { "source": "friend_lite", - "client": self.client_name + "client": self.client_name, + "user_email": self.user_email }, - "infer": True, - "app_id": app_id # Use app_id to avoid duplicate name issues + "infer": True } ) response.raise_for_status() @@ -334,11 +338,101 @@ async def delete_all_memories(self) -> int: return result.get("deleted_count", len(memory_ids)) return len(memory_ids) - + except Exception as e: memory_logger.error(f"Error deleting all memories: {e}") return 0 - + + async def get_memory(self, memory_id: str) -> Optional[Dict[str, Any]]: + """Get a specific memory by ID. + + Args: + memory_id: ID of the memory to retrieve + + Returns: + Memory dictionary if found, None otherwise + """ + try: + # Use the memories endpoint with specific ID + response = await self.client.get( + f"{self.server_url}/api/v1/memories/{memory_id}", + params={"user_id": self.user_id} + ) + + if response.status_code == 404: + memory_logger.warning(f"Memory not found: {memory_id}") + return None + + response.raise_for_status() + result = response.json() + + # Format memory for Friend-Lite + if isinstance(result, dict): + return { + "id": result.get("id", memory_id), + "content": result.get("content", "") or result.get("text", ""), + "metadata": result.get("metadata_", {}) or result.get("metadata", {}), + "created_at": result.get("created_at"), + } + + return None + + except httpx.HTTPStatusError as e: + if e.response.status_code == 404: + return None + memory_logger.error(f"HTTP error getting memory: {e}") + return None + except Exception as e: + memory_logger.error(f"Error getting memory: {e}") + return None + + async def update_memory( + self, + memory_id: str, + content: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None + ) -> bool: + """Update a specific memory's content and/or metadata. + + Args: + memory_id: ID of the memory to update + content: New content for the memory (if None, content is not updated) + metadata: New metadata to merge with existing (if None, metadata is not updated) + + Returns: + True if update succeeded, False otherwise + """ + try: + # Build update payload + update_data: Dict[str, Any] = {"user_id": self.user_id} + + if content is not None: + update_data["text"] = content + + if metadata is not None: + update_data["metadata"] = metadata + + if len(update_data) == 1: # Only user_id + memory_logger.warning("No update data provided") + return False + + # Use PUT to update memory + response = await self.client.put( + f"{self.server_url}/api/v1/memories/{memory_id}", + json=update_data + ) + + response.raise_for_status() + memory_logger.info(f"โœ… Updated OpenMemory memory: {memory_id}") + return True + + except httpx.HTTPStatusError as e: + memory_logger.error(f"HTTP error updating memory: {e.response.status_code}") + return False + except Exception as e: + memory_logger.error(f"Error updating memory: {e}") + return False + async def delete_memory(self, memory_id: str, user_id: Optional[str] = None, user_email: Optional[str] = None) -> bool: """Delete a specific memory by ID. diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py index 3033c307..40184776 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mycelia.py @@ -4,16 +4,41 @@ that uses Mycelia as the backend for all memory operations. """ +import json import logging from datetime import datetime from typing import Any, Dict, List, Optional, Tuple import httpx from ..base import MemoryEntry, MemoryServiceBase +from ..prompts import FACT_RETRIEVAL_PROMPT, TEMPORAL_ENTITY_EXTRACTION_PROMPT, TemporalEntity +from ..config import MemoryConfig +from .llm_providers import _get_openai_client memory_logger = logging.getLogger("memory_service") +def strip_markdown_json(content: str) -> str: + """Strip markdown code block wrapper from JSON content. + + Handles formats like: + - ```json\n{...}\n``` + - ```\n{...}\n``` + - {... } (plain JSON, returned as-is) + """ + content = content.strip() + if content.startswith("```"): + # Remove opening ```json or ``` + first_newline = content.find("\n") + if first_newline != -1: + content = content[first_newline + 1:] + # Remove closing ``` + if content.endswith("```"): + content = content[:-3] + content = content.strip() + return content + + class MyceliaMemoryService(MemoryServiceBase): """Memory service implementation using Mycelia backend. @@ -26,27 +51,23 @@ class MyceliaMemoryService(MemoryServiceBase): **kwargs: Additional configuration parameters """ - def __init__( - self, - api_url: str = "http://localhost:8080", - timeout: int = 30, - **kwargs - ): + def __init__(self, config: MemoryConfig): """Initialize Mycelia memory service. Args: - api_url: Mycelia API endpoint - timeout: Request timeout in seconds - **kwargs: Additional configuration parameters + config: MemoryConfig object containing mycelia_config and llm_config """ - self.api_url = api_url.rstrip("/") - self.timeout = timeout - self.config = kwargs - self._initialized = False + super().__init__() + self.config = config + self.mycelia_config = config.mycelia_config or {} + self.api_url = self.mycelia_config.get("api_url", "http://localhost:8080").rstrip("/") + self.timeout = self.mycelia_config.get("timeout", 30) self._client: Optional[httpx.AsyncClient] = None - memory_logger.info(f"๐Ÿ„ Initializing Mycelia memory service at {api_url}") + # Store LLM config for temporal extraction + self.llm_config = config.llm_config or {} + memory_logger.info(f"๐Ÿ„ Initializing Mycelia memory service at {self.api_url}") async def initialize(self) -> None: """Initialize Mycelia client and verify connection.""" try: @@ -119,21 +140,47 @@ def _mycelia_object_to_memory_entry(self, obj: Dict, user_id: str) -> MemoryEntr user_id: User ID for metadata Returns: - MemoryEntry object + MemoryEntry object with full Mycelia metadata including temporal and semantic fields """ memory_id = self._extract_bson_id(obj.get("_id", "")) memory_content = obj.get("details", "") + # Build metadata with all Mycelia fields + metadata = { + "user_id": user_id, + "name": obj.get("name", ""), + "aliases": obj.get("aliases", []), + "created_at": self._extract_bson_date(obj.get("createdAt")), + "updated_at": self._extract_bson_date(obj.get("updatedAt")), + # Semantic flags + "isPerson": obj.get("isPerson", False), + "isEvent": obj.get("isEvent", False), + "isPromise": obj.get("isPromise", False), + "isRelationship": obj.get("isRelationship", False), + } + + # Add icon if present + if "icon" in obj and obj["icon"]: + metadata["icon"] = obj["icon"] + + # Add temporal information if present + if "timeRanges" in obj and obj["timeRanges"]: + # Convert BSON dates in timeRanges to ISO strings for JSON serialization + time_ranges = [] + for tr in obj["timeRanges"]: + time_range = { + "start": self._extract_bson_date(tr.get("start")), + "end": self._extract_bson_date(tr.get("end")), + } + if "name" in tr: + time_range["name"] = tr["name"] + time_ranges.append(time_range) + metadata["timeRanges"] = time_ranges + return MemoryEntry( id=memory_id, content=memory_content, - metadata={ - "user_id": user_id, - "name": obj.get("name", ""), - "aliases": obj.get("aliases", []), - "created_at": self._extract_bson_date(obj.get("createdAt")), - "updated_at": self._extract_bson_date(obj.get("updatedAt")), - }, + metadata=metadata, created_at=self._extract_bson_date(obj.get("createdAt")) ) @@ -175,6 +222,140 @@ async def _call_resource( memory_logger.error(f"Failed to call Mycelia resource: {e}") raise RuntimeError(f"Mycelia API call failed: {e}") + async def _extract_memories_via_llm( + self, + transcript: str, + ) -> List[str]: + """Extract memories from transcript using OpenAI directly. + + Args: + transcript: Raw transcript text + + Returns: + List of extracted memory facts + + Raises: + RuntimeError: If LLM call fails + """ + if not self.llm_config: + memory_logger.warning("No LLM config available for fact extraction") + return [] + + try: + # Get OpenAI client using Friend-Lite's utility + client = _get_openai_client( + api_key=self.llm_config.get("api_key"), + base_url=self.llm_config.get("base_url", "https://api.openai.com/v1"), + is_async=True + ) + + # Call OpenAI for memory extraction + response = await client.chat.completions.create( + model=self.llm_config.get("model", "gpt-4o-mini"), + messages=[ + {"role": "system", "content": FACT_RETRIEVAL_PROMPT}, + {"role": "user", "content": transcript} + ], + response_format={"type": "json_object"}, + temperature=0.1 + ) + + content = response.choices[0].message.content + + if not content: + memory_logger.warning("LLM returned empty content") + return [] + + # Parse JSON response to extract facts + try: + # Strip markdown wrapper if present (just in case) + json_content = strip_markdown_json(content) + facts_data = json.loads(json_content) + facts = facts_data.get("facts", []) + memory_logger.info(f"๐Ÿง  Extracted {len(facts)} facts from transcript via OpenAI") + return facts + except json.JSONDecodeError as e: + memory_logger.error(f"Failed to parse LLM response as JSON: {e}") + memory_logger.error(f"LLM response was: {content[:300]}") + return [] + + except Exception as e: + memory_logger.error(f"Failed to extract memories via OpenAI: {e}") + raise RuntimeError(f"OpenAI memory extraction failed: {e}") + + async def _extract_temporal_entity_via_llm( + self, + fact: str, + ) -> Optional[TemporalEntity]: + """Extract temporal and entity information from a fact using OpenAI directly. + + Args: + fact: Memory fact text + + Returns: + TemporalEntity with extracted information, or None if extraction fails + """ + if not self.llm_config: + memory_logger.warning("No LLM config available for temporal extraction") + return None + + try: + # Get OpenAI client using Friend-Lite's utility + client = _get_openai_client( + api_key=self.llm_config.get("api_key"), + base_url=self.llm_config.get("base_url", "https://api.openai.com/v1"), + is_async=True + ) + + # Call OpenAI with structured output request + response = await client.chat.completions.create( + model=self.llm_config.get("model", "gpt-4o-mini"), + messages=[ + {"role": "system", "content": TEMPORAL_ENTITY_EXTRACTION_PROMPT}, + {"role": "user", "content": f"Extract temporal and entity information from this memory fact:\n\n{fact}"} + ], + response_format={"type": "json_object"}, + temperature=0.1 + ) + + content = response.choices[0].message.content + + if not content: + memory_logger.warning("LLM returned empty content for temporal extraction") + return None + + # Parse JSON response and validate with Pydantic + try: + # Strip markdown wrapper if present (just in case) + json_content = strip_markdown_json(content) + temporal_data = json.loads(json_content) + + # Convert timeRanges to proper format if present + if "timeRanges" in temporal_data: + for time_range in temporal_data["timeRanges"]: + if isinstance(time_range["start"], str): + time_range["start"] = datetime.fromisoformat(time_range["start"].replace("Z", "+00:00")) + if isinstance(time_range["end"], str): + time_range["end"] = datetime.fromisoformat(time_range["end"].replace("Z", "+00:00")) + + temporal_entity = TemporalEntity(**temporal_data) + memory_logger.info(f"โœ… Temporal extraction: isEvent={temporal_entity.isEvent}, timeRanges={len(temporal_entity.timeRanges)}, entities={temporal_entity.entities}") + return temporal_entity + + except json.JSONDecodeError as e: + memory_logger.error(f"โŒ Failed to parse temporal extraction JSON: {e}") + memory_logger.error(f"Content (first 300 chars): {content[:300]}") + return None + except Exception as e: + memory_logger.error(f"Failed to validate temporal entity: {e}") + memory_logger.error(f"Data: {content[:300] if content else 'None'}") + return None + + except Exception as e: + memory_logger.error(f"Failed to extract temporal data via OpenAI: {e}") + # Don't fail the entire memory creation if temporal extraction fails + return None + async def add_memory( self, transcript: str, @@ -199,37 +380,96 @@ async def add_memory( Returns: Tuple of (success: bool, created_memory_ids: List[str]) """ + # Ensure service is initialized (lazy initialization for RQ workers) + await self._ensure_initialized() + try: # Generate JWT token for this user jwt_token = await self._get_user_jwt(user_id, user_email) - # Create a Mycelia object for this memory - # Memory content is stored in the 'details' field - memory_preview = transcript[:50] + ("..." if len(transcript) > 50 else "") - - object_data = { - "name": f"Memory: {memory_preview}", - "details": transcript, - "aliases": [source_id, client_id], # Searchable by source or client - "isPerson": False, - "isPromise": False, - "isEvent": False, - "isRelationship": False, - # Note: userId is auto-injected by Mycelia from JWT - } + # Extract memories from transcript using OpenAI + memory_logger.info(f"Extracting memories from transcript via OpenAI...") + extracted_facts = await self._extract_memories_via_llm(transcript) - result = await self._call_resource( - action="create", - jwt_token=jwt_token, - object=object_data - ) + if not extracted_facts: + memory_logger.warning("No memories extracted from transcript") + return (False, []) - memory_id = result.get("insertedId") - if memory_id: - memory_logger.info(f"โœ… Created Mycelia memory object: {memory_id}") - return (True, [memory_id]) + # Create Mycelia objects for each extracted fact + memory_ids = [] + for fact in extracted_facts: + fact_preview = fact[:50] + ("..." if len(fact) > 50 else "") + + # Extract temporal and entity information + temporal_entity = await self._extract_temporal_entity_via_llm(fact) + + # Build object data with temporal/entity information if available + if temporal_entity: + # Convert timeRanges from Pydantic models to dict format for Mycelia API + time_ranges = [] + for tr in temporal_entity.timeRanges: + time_range_dict = { + "start": tr.start.isoformat() if isinstance(tr.start, datetime) else tr.start, + "end": tr.end.isoformat() if isinstance(tr.end, datetime) else tr.end, + } + if tr.name: + time_range_dict["name"] = tr.name + time_ranges.append(time_range_dict) + + # Use emoji in name if available, otherwise use default + name_prefix = temporal_entity.emoji if temporal_entity.emoji else "Memory:" + + object_data = { + "name": f"{name_prefix} {fact_preview}", + "details": fact, + "aliases": [source_id, client_id] + temporal_entity.entities, # Include extracted entities + "isPerson": temporal_entity.isPerson, + "isPromise": temporal_entity.isPromise, + "isEvent": temporal_entity.isEvent, + "isRelationship": temporal_entity.isRelationship, + # Note: userId is auto-injected by Mycelia from JWT + } + + # Add timeRanges if temporal information was extracted + if time_ranges: + object_data["timeRanges"] = time_ranges + + # Add emoji icon if available + if temporal_entity.emoji: + object_data["icon"] = {"text": temporal_entity.emoji} + + memory_logger.info(f"๐Ÿ“… Temporal extraction: isEvent={temporal_entity.isEvent}, timeRanges={len(time_ranges)}, entities={len(temporal_entity.entities)}") + else: + # Fallback to basic object without temporal data + object_data = { + "name": f"Memory: {fact_preview}", + "details": fact, + "aliases": [source_id, client_id], + "isPerson": False, + "isPromise": False, + "isEvent": False, + "isRelationship": False, + } + memory_logger.warning(f"โš ๏ธ No temporal data extracted for fact: {fact_preview}") + + result = await self._call_resource( + action="create", + jwt_token=jwt_token, + object=object_data + ) + + memory_id = result.get("insertedId") + if memory_id: + memory_logger.info(f"โœ… Created Mycelia memory object: {memory_id} - {fact_preview}") + memory_ids.append(memory_id) + else: + memory_logger.error(f"Failed to create memory fact: {fact}") + + if memory_ids: + memory_logger.info(f"โœ… Created {len(memory_ids)} Mycelia memory objects from {len(extracted_facts)} facts") + return (True, memory_ids) else: - memory_logger.error("Failed to create Mycelia memory: no insertedId returned") + memory_logger.error("No Mycelia memory objects were created") return (False, []) except Exception as e: @@ -362,6 +602,126 @@ async def count_memories(self, user_id: str) -> Optional[int]: memory_logger.error(f"Failed to count memories via Mycelia: {e}") return None + async def get_memory(self, memory_id: str, user_id: Optional[str] = None) -> Optional[MemoryEntry]: + """Get a specific memory by ID from Mycelia. + + Args: + memory_id: Unique identifier of the memory to retrieve + user_id: Optional user identifier for authentication + + Returns: + MemoryEntry object if found, None otherwise + """ + if not self._initialized: + await self.initialize() + + try: + # Need user ID for JWT authentication + if not user_id: + memory_logger.error("User ID required for Mycelia get_memory operation") + return None + + # Generate JWT token for this user + jwt_token = await self._get_user_jwt(user_id) + + # Get the object by ID (auto-scoped by userId in Mycelia) + result = await self._call_resource( + action="get", + jwt_token=jwt_token, + id=memory_id + ) + + if result: + return self._mycelia_object_to_memory_entry(result, user_id) + else: + memory_logger.warning(f"Memory not found with ID: {memory_id}") + return None + + except Exception as e: + memory_logger.error(f"Failed to get memory via Mycelia: {e}") + return None + + async def update_memory( + self, + memory_id: str, + content: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + user_id: Optional[str] = None, + user_email: Optional[str] = None + ) -> bool: + """Update a specific memory's content and/or metadata in Mycelia. + + Args: + memory_id: Unique identifier of the memory to update + content: New content for the memory (updates 'details' field) + metadata: New metadata to merge with existing + user_id: Optional user ID for authentication + user_email: Optional user email for authentication + + Returns: + True if update succeeded, False otherwise + """ + if not self._initialized: + await self.initialize() + + try: + # Need user ID for JWT authentication + if not user_id: + memory_logger.error("User ID required for Mycelia update_memory operation") + return False + + # Generate JWT token for this user + jwt_token = await self._get_user_jwt(user_id, user_email) + + # Build update object + update_data: Dict[str, Any] = {} + + if content is not None: + update_data["details"] = content + + if metadata: + # Extract specific metadata fields that Mycelia supports + if "name" in metadata: + update_data["name"] = metadata["name"] + if "aliases" in metadata: + update_data["aliases"] = metadata["aliases"] + if "isPerson" in metadata: + update_data["isPerson"] = metadata["isPerson"] + if "isPromise" in metadata: + update_data["isPromise"] = metadata["isPromise"] + if "isEvent" in metadata: + update_data["isEvent"] = metadata["isEvent"] + if "isRelationship" in metadata: + update_data["isRelationship"] = metadata["isRelationship"] + if "timeRanges" in metadata: + update_data["timeRanges"] = metadata["timeRanges"] + if "icon" in metadata: + update_data["icon"] = metadata["icon"] + + if not update_data: + memory_logger.warning("No update data provided") + return False + + # Update the object (auto-scoped by userId in Mycelia) + result = await self._call_resource( + action="update", + jwt_token=jwt_token, + id=memory_id, + object=update_data + ) + + updated_count = result.get("modifiedCount", 0) + if updated_count > 0: + memory_logger.info(f"โœ… Updated Mycelia memory object: {memory_id}") + return True + else: + memory_logger.warning(f"No memory updated with ID: {memory_id}") + return False + + except Exception as e: + memory_logger.error(f"Failed to update memory via Mycelia: {e}") + return False + async def delete_memory(self, memory_id: str, user_id: Optional[str] = None, user_email: Optional[str] = None) -> bool: """Delete a specific memory from Mycelia. diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py index 04b8fd67..d18be16a 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py @@ -47,30 +47,26 @@ def __init__( user_id: Optional[str] = None, timeout: Optional[int] = None, ): - self.server_url = server_url or os.getenv("OPENMEMORY_MCP_URL", "http://localhost:8765") - self.client_name = client_name or os.getenv("OPENMEMORY_CLIENT_NAME", "friend_lite") - self.user_id = user_id or os.getenv("OPENMEMORY_USER_ID", "default") - self.timeout = int(timeout or os.getenv("OPENMEMORY_TIMEOUT", "30")) """Initialize OpenMemory MCP service as a thin client. - + This service delegates all memory processing to the OpenMemory MCP server: - Memory extraction (OpenMemory handles internally) - - Deduplication (OpenMemory handles internally) + - Deduplication (OpenMemory handles internally) - Vector storage (OpenMemory handles internally) - User isolation via ACL (OpenMemory handles internally) - + Args: server_url: URL of the OpenMemory MCP server (default: http://localhost:8765) client_name: Client identifier for OpenMemory MCP user_id: User identifier for memory isolation via OpenMemory ACL timeout: HTTP request timeout in seconds """ - self.server_url = server_url - self.client_name = client_name - self.user_id = user_id - self.timeout = timeout + super().__init__() + self.server_url = server_url or os.getenv("OPENMEMORY_MCP_URL", "http://localhost:8765") + self.client_name = client_name or os.getenv("OPENMEMORY_CLIENT_NAME", "friend_lite") + self.user_id = user_id or os.getenv("OPENMEMORY_USER_ID", "default") + self.timeout = int(timeout or os.getenv("OPENMEMORY_TIMEOUT", "30")) self.mcp_client: Optional[MCPClient] = None - self._initialized = False async def initialize(self) -> None: """Initialize the OpenMemory MCP service. @@ -138,8 +134,7 @@ async def add_memory( Raises: MCPError: If MCP server communication fails """ - if not self._initialized: - await self.initialize() + await self._ensure_initialized() try: # Skip empty transcripts @@ -149,19 +144,22 @@ async def add_memory( # Update MCP client user context for this operation original_user_id = self.mcp_client.user_id - self.mcp_client.user_id = self.user_id # Use configured user ID - + original_user_email = self.mcp_client.user_email + self.mcp_client.user_id = user_id # Use the actual Friend-Lite user's ID + self.mcp_client.user_email = user_email # Use the actual user's email + try: # Thin client approach: Send raw transcript to OpenMemory MCP server # OpenMemory handles: extraction, deduplication, vector storage, ACL enriched_transcript = f"[Source: {source_id}, Client: {client_id}] {transcript}" - - memory_logger.info(f"Delegating memory processing to OpenMemory MCP for {source_id}") + + memory_logger.info(f"Delegating memory processing to OpenMemory MCP for user {user_id}, source {source_id}") memory_ids = await self.mcp_client.add_memories(text=enriched_transcript) - + finally: - # Restore original user_id + # Restore original user context self.mcp_client.user_id = original_user_id + self.mcp_client.user_email = original_user_email # Update database relationships if helper provided if memory_ids and db_helper: @@ -208,24 +206,24 @@ async def search_memories( # Update MCP client user context for this operation original_user_id = self.mcp_client.user_id - self.mcp_client.user_id = self.user_id # Use configured user ID - + self.mcp_client.user_id = user_id # Use the actual Friend-Lite user's ID + try: results = await self.mcp_client.search_memory( query=query, limit=limit ) - + # Convert MCP results to MemoryEntry objects memory_entries = [] for result in results: memory_entry = self._mcp_result_to_memory_entry(result, user_id) if memory_entry: memory_entries.append(memory_entry) - + memory_logger.info(f"๐Ÿ” Found {len(memory_entries)} memories for query '{query}' (user: {user_id})") return memory_entries - + except MCPError as e: memory_logger.error(f"Search memories failed: {e}") return [] @@ -258,21 +256,21 @@ async def get_all_memories( # Update MCP client user context for this operation original_user_id = self.mcp_client.user_id - self.mcp_client.user_id = self.user_id # Use configured user ID - + self.mcp_client.user_id = user_id # Use the actual Friend-Lite user's ID + try: results = await self.mcp_client.list_memories(limit=limit) - + # Convert MCP results to MemoryEntry objects memory_entries = [] for result in results: memory_entry = self._mcp_result_to_memory_entry(result, user_id) if memory_entry: memory_entries.append(memory_entry) - + memory_logger.info(f"๐Ÿ“š Retrieved {len(memory_entries)} memories for user {user_id}") return memory_entries - + except MCPError as e: memory_logger.error(f"Get all memories failed: {e}") return [] @@ -282,7 +280,89 @@ async def get_all_memories( finally: # Restore original user_id self.mcp_client.user_id = original_user_id - + + async def get_memory(self, memory_id: str, user_id: Optional[str] = None) -> Optional[MemoryEntry]: + """Get a specific memory by ID. + + Args: + memory_id: Unique identifier of the memory to retrieve + user_id: Optional user identifier for filtering + + Returns: + MemoryEntry object if found, None otherwise + """ + if not self._initialized: + await self.initialize() + + # Update MCP client user context for this operation + original_user_id = self.mcp_client.user_id + self.mcp_client.user_id = user_id or self.user_id # Use the actual Friend-Lite user's ID + + try: + result = await self.mcp_client.get_memory(memory_id) + + if not result: + memory_logger.warning(f"Memory not found: {memory_id}") + return None + + # Convert MCP result to MemoryEntry + memory_entry = self._mcp_result_to_memory_entry(result, user_id or self.user_id) + if memory_entry: + memory_logger.info(f"๐Ÿ“– Retrieved memory {memory_id}") + return memory_entry + + except Exception as e: + memory_logger.error(f"Failed to get memory: {e}") + return None + finally: + # Restore original user_id + self.mcp_client.user_id = original_user_id + + async def update_memory( + self, + memory_id: str, + content: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + user_id: Optional[str] = None, + user_email: Optional[str] = None + ) -> bool: + """Update a specific memory's content and/or metadata. + + Args: + memory_id: Unique identifier of the memory to update + content: New content for the memory (if None, content is not updated) + metadata: New metadata to merge with existing (if None, metadata is not updated) + user_id: Optional user ID (not used by OpenMemory MCP) + user_email: Optional user email (not used by OpenMemory MCP) + + Returns: + True if update succeeded, False otherwise + """ + if not self._initialized: + await self.initialize() + + # Update MCP client user context for this operation + original_user_id = self.mcp_client.user_id + self.mcp_client.user_id = user_id or self.user_id # Use the actual Friend-Lite user's ID + + try: + success = await self.mcp_client.update_memory( + memory_id=memory_id, + content=content, + metadata=metadata + ) + + if success: + memory_logger.info(f"โœ๏ธ Updated memory {memory_id} via MCP") + return success + + except Exception as e: + memory_logger.error(f"Failed to update memory: {e}") + return False + finally: + # Restore original user_id + self.mcp_client.user_id = original_user_id + async def delete_memory(self, memory_id: str, user_id: Optional[str] = None, user_email: Optional[str] = None) -> bool: """Delete a specific memory by ID. diff --git a/backends/advanced/src/advanced_omi_backend/workers/memory_jobs.py b/backends/advanced/src/advanced_omi_backend/workers/memory_jobs.py index 6b8da757..fdb16b7d 100644 --- a/backends/advanced/src/advanced_omi_backend/workers/memory_jobs.py +++ b/backends/advanced/src/advanced_omi_backend/workers/memory_jobs.py @@ -180,9 +180,11 @@ async def process_memory_job( for memory_id in created_memory_ids[:5]: # Limit to first 5 for display memory_entry = await memory_service.get_memory(memory_id, user_id) if memory_entry: + # memory_entry is a MemoryEntry object, not a dict + memory_text = memory_entry.content if hasattr(memory_entry, 'content') else str(memory_entry) memory_details.append({ "memory_id": memory_id, - "text": memory_entry.get("text", "")[:200] # First 200 chars + "text": memory_text[:200] # First 200 chars }) except Exception as e: logger.warning(f"Failed to fetch memory details for UI: {e}") From 0f51fcefeae7dd16e31ba221b5005104c7c18258 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Wed, 3 Dec 2025 22:57:48 +0000 Subject: [PATCH 08/21] added memory provider switch --- .../controllers/system_controller.py | 102 ++++++++++++++++- .../routers/modules/system_routes.py | 17 +++ backends/advanced/webui/src/pages/System.tsx | 103 +++++++++++++++++- 3 files changed, 217 insertions(+), 5 deletions(-) diff --git a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py index a2afadbc..9341cc59 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py @@ -445,19 +445,19 @@ async def delete_all_user_memories(user: User): from advanced_omi_backend.services.memory import get_memory_service memory_service = get_memory_service() - + # Delete all memories for the user deleted_count = await memory_service.delete_all_user_memories(user.user_id) - + logger.info(f"Deleted {deleted_count} memories for user {user.user_id}") - + return { "message": f"Successfully deleted {deleted_count} memories", "deleted_count": deleted_count, "user_id": user.user_id, "status": "success" } - + except Exception as e: logger.error(f"Error deleting all memories for user {user.user_id}: {e}") return JSONResponse( @@ -465,3 +465,97 @@ async def delete_all_user_memories(user: User): ) +# Memory Provider Configuration Functions + +async def get_memory_provider(): + """Get current memory provider configuration.""" + try: + current_provider = os.getenv("MEMORY_PROVIDER", "friend_lite").lower() + + # Get available providers + available_providers = ["friend_lite", "openmemory_mcp", "mycelia"] + + return { + "current_provider": current_provider, + "available_providers": available_providers, + "status": "success" + } + + except Exception as e: + logger.error(f"Error getting memory provider: {e}") + return JSONResponse( + status_code=500, content={"error": f"Failed to get memory provider: {str(e)}"} + ) + + +async def set_memory_provider(provider: str): + """Set memory provider and update .env file.""" + try: + # Validate provider + provider = provider.lower().strip() + valid_providers = ["friend_lite", "openmemory_mcp", "mycelia"] + + if provider not in valid_providers: + return JSONResponse( + status_code=400, + content={"error": f"Invalid provider '{provider}'. Valid providers: {', '.join(valid_providers)}"} + ) + + # Path to .env file (assuming we're running from backends/advanced/) + env_path = os.path.join(os.getcwd(), ".env") + + if not os.path.exists(env_path): + return JSONResponse( + status_code=404, + content={"error": f".env file not found at {env_path}"} + ) + + # Read current .env file + with open(env_path, 'r') as file: + lines = file.readlines() + + # Update or add MEMORY_PROVIDER line + provider_found = False + updated_lines = [] + + for line in lines: + if line.strip().startswith("MEMORY_PROVIDER="): + updated_lines.append(f"MEMORY_PROVIDER={provider}\n") + provider_found = True + else: + updated_lines.append(line) + + # If MEMORY_PROVIDER wasn't found, add it + if not provider_found: + updated_lines.append(f"\n# Memory Provider Configuration\nMEMORY_PROVIDER={provider}\n") + + # Create backup + backup_path = f"{env_path}.bak" + shutil.copy2(env_path, backup_path) + logger.info(f"Created .env backup at {backup_path}") + + # Write updated .env file + with open(env_path, 'w') as file: + file.writelines(updated_lines) + + # Update environment variable for current process + os.environ["MEMORY_PROVIDER"] = provider + + logger.info(f"Updated MEMORY_PROVIDER to '{provider}' in .env file") + + return { + "message": f"Memory provider updated to '{provider}'. Please restart the backend service for changes to take effect.", + "provider": provider, + "env_path": env_path, + "backup_created": True, + "requires_restart": True, + "status": "success" + } + + except Exception as e: + logger.error(f"Error setting memory provider: {e}") + return JSONResponse( + status_code=500, content={"error": f"Failed to set memory provider: {str(e)}"} + ) + + diff --git a/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py b/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py index 3c97bd55..10587b5c 100644 --- a/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py +++ b/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py @@ -126,3 +126,20 @@ async def cleanup_stuck_stream_workers(request: Request, current_user: User = De async def cleanup_old_sessions(request: Request, max_age_seconds: int = 3600, current_user: User = Depends(current_superuser)): """Clean up old session tracking metadata. Admin only.""" return await session_controller.cleanup_old_sessions(request, max_age_seconds) + + +# Memory Provider Configuration Endpoints + +@router.get("/admin/memory/provider") +async def get_memory_provider(current_user: User = Depends(current_superuser)): + """Get current memory provider configuration. Admin only.""" + return await system_controller.get_memory_provider() + + +@router.post("/admin/memory/provider") +async def set_memory_provider( + provider: str = Body(..., embed=True), + current_user: User = Depends(current_superuser) +): + """Set memory provider and restart backend services. Admin only.""" + return await system_controller.set_memory_provider(provider) diff --git a/backends/advanced/webui/src/pages/System.tsx b/backends/advanced/webui/src/pages/System.tsx index 3ca54a59..c722ada9 100644 --- a/backends/advanced/webui/src/pages/System.tsx +++ b/backends/advanced/webui/src/pages/System.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { Settings, RefreshCw, CheckCircle, XCircle, AlertCircle, Activity, Users, Database, Server, Volume2, Mic } from 'lucide-react' +import { Settings, RefreshCw, CheckCircle, XCircle, AlertCircle, Activity, Users, Database, Server, Volume2, Mic, Brain } from 'lucide-react' import { systemApi, speakerApi } from '../services/api' import { useAuth } from '../contexts/AuthContext' import MemorySettings from '../components/MemorySettings' @@ -64,6 +64,11 @@ export default function System() { max_speakers: 6 }) const [diarizationLoading, setDiarizationLoading] = useState(false) + const [currentProvider, setCurrentProvider] = useState('') + const [availableProviders, setAvailableProviders] = useState([]) + const [selectedProvider, setSelectedProvider] = useState('') + const [providerLoading, setProviderLoading] = useState(false) + const [providerMessage, setProviderMessage] = useState('') const { isAdmin } = useAuth() @@ -120,6 +125,46 @@ export default function System() { } } + const loadMemoryProvider = async () => { + try { + setProviderLoading(true) + const response = await systemApi.getMemoryProvider() + if (response.data.status === 'success') { + setCurrentProvider(response.data.current_provider) + setAvailableProviders(response.data.available_providers) + setSelectedProvider(response.data.current_provider) + } + } catch (err: any) { + console.error('Failed to load memory provider:', err) + } finally { + setProviderLoading(false) + } + } + + const saveMemoryProvider = async () => { + if (selectedProvider === currentProvider) { + setProviderMessage('Provider is already set to ' + selectedProvider) + setTimeout(() => setProviderMessage(''), 3000) + return + } + + try { + setProviderLoading(true) + setProviderMessage('') + const response = await systemApi.setMemoryProvider(selectedProvider) + if (response.data.status === 'success') { + setCurrentProvider(selectedProvider) + setProviderMessage('โœ… ' + response.data.message) + } else { + setProviderMessage('โŒ Failed to update provider') + } + } catch (err: any) { + setProviderMessage('โŒ Error: ' + (err.response?.data?.error || err.message)) + } finally { + setProviderLoading(false) + } + } + const saveDiarizationSettings = async () => { try { setDiarizationLoading(true) @@ -139,6 +184,7 @@ export default function System() { useEffect(() => { loadSystemData() loadDiarizationSettings() + loadMemoryProvider() }, [isAdmin]) const getStatusIcon = (healthy: boolean) => { @@ -285,6 +331,61 @@ export default function System() { ))} + + {/* Memory Provider Selector */} +
+
+ + + Memory Provider + +
+
+ {/* Current Provider Display */} +
+ Current: + + {currentProvider || 'Loading...'} + +
+ + {/* Provider Selector */} +
+ + +
+ + {/* Status Message */} + {providerMessage && ( +
+ {providerMessage} +
+ )} +
+
)} From d92dc213b800e61b44a616792d7a463d47d9d413 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Wed, 3 Dec 2025 23:01:06 +0000 Subject: [PATCH 09/21] added clickable memories --- .../advanced/webui/src/pages/Memories.tsx | 184 +++++----- .../advanced/webui/src/pages/MemoryDetail.tsx | 331 ++++++++++++++++++ 2 files changed, 435 insertions(+), 80 deletions(-) create mode 100644 backends/advanced/webui/src/pages/MemoryDetail.tsx diff --git a/backends/advanced/webui/src/pages/Memories.tsx b/backends/advanced/webui/src/pages/Memories.tsx index 7ad3bf59..0c4973b6 100644 --- a/backends/advanced/webui/src/pages/Memories.tsx +++ b/backends/advanced/webui/src/pages/Memories.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' import { Brain, Search, RefreshCw, Trash2, Calendar, Tag, X, Target } from 'lucide-react' import { memoriesApi, systemApi } from '../services/api' import { useAuth } from '../contexts/AuthContext' @@ -18,24 +19,25 @@ interface Memory { } export default function Memories() { + const navigate = useNavigate() const [memories, setMemories] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [showUnfiltered, setShowUnfiltered] = useState(false) const [totalCount, setTotalCount] = useState(null) - + // Semantic search state const [semanticResults, setSemanticResults] = useState([]) const [isSemanticFilterActive, setIsSemanticFilterActive] = useState(false) const [semanticQuery, setSemanticQuery] = useState('') const [semanticLoading, setSemanticLoading] = useState(false) const [relevanceThreshold, setRelevanceThreshold] = useState(0) // 0-100 percentage - + // System configuration state const [memoryProviderSupportsThreshold, setMemoryProviderSupportsThreshold] = useState(false) const [memoryProvider, setMemoryProvider] = useState('') - + const { user } = useAuth() const loadSystemConfig = async () => { @@ -59,24 +61,24 @@ export default function Memories() { try { setLoading(true) - const response = showUnfiltered + const response = showUnfiltered ? await memoriesApi.getUnfiltered(user.id) : await memoriesApi.getAll(user.id) - + console.log('๐Ÿง  Memories API response:', response.data) - + // Handle the API response structure const memoriesData = response.data.memories || response.data || [] const totalCount = response.data.total_count console.log('๐Ÿง  Processed memories data:', memoriesData) console.log('๐Ÿง  Total count:', totalCount) - + // Log first few memories to inspect structure if (memoriesData.length > 0) { console.log('๐Ÿง  First memory object:', memoriesData[0]) console.log('๐Ÿง  Memory fields:', Object.keys(memoriesData[0])) } - + setMemories(Array.isArray(memoriesData) ? memoriesData : []) // Store total count in state for display setTotalCount(totalCount) @@ -100,25 +102,25 @@ export default function Memories() { // Semantic search handlers const handleSemanticSearch = async () => { if (!searchQuery.trim() || !user?.id) return - + try { setSemanticLoading(true) - + // Use current threshold for server-side filtering if memory provider supports it - const thresholdToUse = memoryProviderSupportsThreshold - ? relevanceThreshold + const thresholdToUse = memoryProviderSupportsThreshold + ? relevanceThreshold : undefined - + const response = await memoriesApi.search( - searchQuery.trim(), - user.id, - 50, + searchQuery.trim(), + user.id, + 50, thresholdToUse ) - + console.log('๐Ÿ” Search response:', response.data) console.log('๐ŸŽฏ Used threshold:', thresholdToUse) - + setSemanticResults(response.data.results || []) setSemanticQuery(searchQuery.trim()) setIsSemanticFilterActive(true) @@ -156,7 +158,7 @@ export default function Memories() { // Update filtering logic with client-side threshold filtering after search const currentMemories = isSemanticFilterActive ? semanticResults : memories - + // Apply relevance threshold filter (client-side for all providers after search) const thresholdFilteredMemories = isSemanticFilterActive && relevanceThreshold > 0 ? currentMemories.filter(memory => { @@ -165,7 +167,7 @@ export default function Memories() { return relevancePercentage >= relevanceThreshold }) : currentMemories - + // Apply text search filter const filteredMemories = thresholdFilteredMemories.filter(memory => memory.memory.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -175,7 +177,7 @@ export default function Memories() { const formatDate = (dateInput: string | number) => { // Handle both timestamp numbers and date strings let date: Date - + if (typeof dateInput === 'number') { // Unix timestamp - multiply by 1000 if needed date = dateInput > 1e10 ? new Date(dateInput) : new Date(dateInput * 1000) @@ -192,20 +194,20 @@ export default function Memories() { } else { date = new Date(dateInput) } - + // Check if date is valid if (isNaN(date.getTime())) { console.warn('Invalid date:', dateInput) return 'Invalid Date' } - + return date.toLocaleString() } const getCategoryColor = (category: string) => { const colors = { 'personal': 'bg-blue-100 text-blue-800', - 'work': 'bg-green-100 text-green-800', + 'work': 'bg-green-100 text-green-800', 'health': 'bg-red-100 text-red-800', 'entertainment': 'bg-purple-100 text-purple-800', 'education': 'bg-yellow-100 text-yellow-800', @@ -218,7 +220,7 @@ export default function Memories() { const renderMemoryText = (content: string) => { // Handle multi-line content (bullet points from backend normalization) const lines = content.split('\n').filter(line => line.trim()) - + if (lines.length > 1) { return (
@@ -230,7 +232,7 @@ export default function Memories() {
) } - + // Single line content return (

@@ -298,7 +300,7 @@ export default function Memories() { onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search memories..." className="w-full pl-10 pr-32 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" - onKeyPress={(e) => e.key === 'Enter' && handleSemanticSearch()} + onKeyDown={(e) => e.key === 'Enter' && handleSemanticSearch()} /> - - {/* Memory Content */} -

- {renderMemoryContent(memory)} -
- - {/* Metadata */} - {memory.metadata && ( -
-
- - View metadata - -
-                        {JSON.stringify(memory.metadata, null, 2)}
-                      
-
-
- )} ))} @@ -550,7 +574,7 @@ export default function Memories() { `No semantic matches found for "${semanticQuery}"` ) ) : ( - searchQuery + searchQuery ? `No memories found matching "${searchQuery}"` : `No memories found` )} @@ -567,4 +591,4 @@ export default function Memories() { )} ) -} \ No newline at end of file +} diff --git a/backends/advanced/webui/src/pages/MemoryDetail.tsx b/backends/advanced/webui/src/pages/MemoryDetail.tsx new file mode 100644 index 00000000..73750958 --- /dev/null +++ b/backends/advanced/webui/src/pages/MemoryDetail.tsx @@ -0,0 +1,331 @@ +import { useState, useEffect } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { ArrowLeft, Calendar, Tag, Trash2, RefreshCw } from 'lucide-react' +import { memoriesApi } from '../services/api' +import { useAuth } from '../contexts/AuthContext' + +interface Memory { + id: string + memory: string + category?: string + created_at: string + updated_at: string + user_id: string + score?: number + metadata?: { + name?: string + timeRanges?: Array<{ + start: string + end: string + name?: string + }> + isPerson?: boolean + isEvent?: boolean + isPlace?: boolean + extractedWith?: { + model: string + timestamp: string + } + [key: string]: any + } + hash?: string + role?: string +} + +export default function MemoryDetail() { + const { id } = useParams<{ id: string }>() + const navigate = useNavigate() + const { user } = useAuth() + const [memory, setMemory] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const loadMemory = async () => { + if (!user?.id || !id) { + console.log('โญ๏ธ MemoryDetail: Missing user or id', { userId: user?.id, memoryId: id }) + return + } + + try { + console.log('๐Ÿ” MemoryDetail: Loading memory', id) + setLoading(true) + setError(null) + const response = await memoriesApi.getAll(user.id) + const memoriesData = response.data.memories || response.data || [] + console.log('๐Ÿ“ฆ MemoryDetail: Loaded memories', memoriesData.length) + + // Find the specific memory by ID + const foundMemory = memoriesData.find((m: Memory) => m.id === id) + console.log('๐ŸŽฏ MemoryDetail: Found memory?', !!foundMemory, foundMemory?.id) + + if (foundMemory) { + setMemory(foundMemory) + } else { + setError('Memory not found') + } + } catch (err: any) { + console.error('โŒ Failed to load memory:', err) + setError(err.message || 'Failed to load memory') + } finally { + setLoading(false) + } + } + + const handleDelete = async () => { + if (!memory || !id) return + + const confirmed = window.confirm('Are you sure you want to delete this memory?') + if (!confirmed) return + + try { + await memoriesApi.delete(id) + navigate('/memories') + } catch (err: any) { + console.error('โŒ Failed to delete memory:', err) + alert('Failed to delete memory: ' + (err.message || 'Unknown error')) + } + } + + useEffect(() => { + loadMemory() + }, [id, user?.id]) + + const formatDate = (dateString: string) => { + try { + return new Date(dateString).toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } catch { + return dateString + } + } + + const getMemoryTypeIcon = () => { + if (memory?.metadata?.isEvent) return '๐Ÿ“…' + if (memory?.metadata?.isPerson) return '๐Ÿ‘ค' + if (memory?.metadata?.isPlace) return '๐Ÿ“' + return '๐Ÿง ' + } + + const getMemoryTypeLabel = () => { + if (memory?.metadata?.isEvent) return 'Event' + if (memory?.metadata?.isPerson) return 'Person' + if (memory?.metadata?.isPlace) return 'Place' + return 'Memory' + } + + if (loading) { + return ( +
+
+ +
+
+ + Loading memory... +
+
+ ) + } + + if (error || !memory) { + return ( +
+
+ +
+
+

+ {error || 'Memory not found'} +

+
+
+ ) + } + + return ( +
+ {/* Header */} +
+ + +
+ + {/* Main Content */} +
+ {/* Left Column - Memory Content */} +
+ {/* Memory Card */} +
+
+
{getMemoryTypeIcon()}
+
+
+ + {getMemoryTypeLabel()} + + {memory.category && ( + + + {memory.category} + + )} +
+ {memory.metadata?.name && ( +

+ {memory.metadata.name} +

+ )} +

+ {memory.memory} +

+
+
+
+ + {/* Time Ranges */} + {memory.metadata?.timeRanges && memory.metadata.timeRanges.length > 0 && ( +
+

+ + Time Ranges +

+
+ {memory.metadata.timeRanges.map((range, index) => ( +
+ +
+ {range.name && ( +
+ {range.name} +
+ )} +
+
Start: {formatDate(range.start)}
+
End: {formatDate(range.end)}
+
+
+
+ ))} +
+
+ )} +
+ + {/* Right Column - Metadata */} +
+ {/* Metadata Card */} +
+

+ Metadata +

+
+
+
Created:
+
+ {formatDate(memory.created_at)} +
+
+
+
Updated:
+
+ {formatDate(memory.updated_at)} +
+
+ {memory.score !== undefined && memory.score !== null && ( +
+
Score:
+
+ {memory.score.toFixed(3)} +
+
+ )} + {memory.hash && ( +
+
Hash:
+
+ {memory.hash.substring(0, 12)}... +
+
+ )} +
+
+ + {/* Extraction Metadata */} + {memory.metadata?.extractedWith && ( +
+

+ Extraction +

+
+
+
Model:
+
+ {memory.metadata.extractedWith.model} +
+
+
+
Time:
+
+ {formatDate(memory.metadata.extractedWith.timestamp)} +
+
+
+
+ )} + + {/* Additional Metadata */} + {memory.metadata && Object.keys(memory.metadata).filter(key => + !['name', 'timeRanges', 'isPerson', 'isEvent', 'isPlace', 'extractedWith'].includes(key) + ).length > 0 && ( +
+

+ Additional Data +

+
+ {Object.entries(memory.metadata) + .filter(([key]) => !['name', 'timeRanges', 'isPerson', 'isEvent', 'isPlace', 'extractedWith'].includes(key)) + .map(([key, value]) => ( +
+
{key}:
+
+ {typeof value === 'object' ? JSON.stringify(value) : String(value)} +
+
+ ))} +
+
+ )} +
+
+
+ ) +} From 45add2edfb98a9a724c99ce03c1e6dec98a5e10b Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Wed, 3 Dec 2025 23:02:47 +0000 Subject: [PATCH 10/21] added timeline views --- .../advanced/webui/public/frappe-gantt.css | 1 + backends/advanced/webui/src/App.tsx | 18 +- .../webui/src/components/layout/Layout.tsx | 3 +- .../webui/src/hooks/useAudioRecording.ts | 4 +- .../advanced/webui/src/hooks/useD3Zoom.ts | 82 ++ .../src/hooks/useSimpleAudioRecording.ts | 4 +- .../webui/src/pages/ConversationsRouter.tsx | 48 ++ .../webui/src/pages/ConversationsTimeline.tsx | 321 ++++++++ .../webui/src/pages/FrappeGanttTimeline.tsx | 707 ++++++++++++++++++ .../webui/src/pages/MyceliaTimeline.tsx | 441 +++++++++++ .../webui/src/pages/ReactGanttTimeline.tsx | 359 +++++++++ .../webui/src/pages/TimelineRouter.tsx | 86 +++ backends/advanced/webui/src/services/api.ts | 8 +- .../webui/src/types/react-gantt-timeline.d.ts | 45 ++ 14 files changed, 2117 insertions(+), 10 deletions(-) create mode 100644 backends/advanced/webui/public/frappe-gantt.css create mode 100644 backends/advanced/webui/src/hooks/useD3Zoom.ts create mode 100644 backends/advanced/webui/src/pages/ConversationsRouter.tsx create mode 100644 backends/advanced/webui/src/pages/ConversationsTimeline.tsx create mode 100644 backends/advanced/webui/src/pages/FrappeGanttTimeline.tsx create mode 100644 backends/advanced/webui/src/pages/MyceliaTimeline.tsx create mode 100644 backends/advanced/webui/src/pages/ReactGanttTimeline.tsx create mode 100644 backends/advanced/webui/src/pages/TimelineRouter.tsx create mode 100644 backends/advanced/webui/src/types/react-gantt-timeline.d.ts diff --git a/backends/advanced/webui/public/frappe-gantt.css b/backends/advanced/webui/public/frappe-gantt.css new file mode 100644 index 00000000..73d5781b --- /dev/null +++ b/backends/advanced/webui/public/frappe-gantt.css @@ -0,0 +1 @@ +:root{--g-arrow-color: #1f2937;--g-bar-color: #fff;--g-bar-border: #fff;--g-tick-color-thick: #ededed;--g-tick-color: #f3f3f3;--g-actions-background: #f3f3f3;--g-border-color: #ebeff2;--g-text-muted: #7c7c7c;--g-text-light: #fff;--g-text-dark: #171717;--g-progress-color: #dbdbdb;--g-handle-color: #37352f;--g-weekend-label-color: #dcdce4;--g-expected-progress: #c4c4e9;--g-header-background: #fff;--g-row-color: #fdfdfd;--g-row-border-color: #c7c7c7;--g-today-highlight: #37352f;--g-popup-actions: #ebeff2;--g-weekend-highlight-color: #f7f7f7}.gantt-container{line-height:14.5px;position:relative;overflow:auto;font-size:12px;height:var(--gv-grid-height);width:100%;border-radius:8px}.gantt-container .popup-wrapper{position:absolute;top:0;left:0;background:#fff;box-shadow:0 10px 24px -3px #0003;padding:10px;border-radius:5px;width:max-content;z-index:1000}.gantt-container .popup-wrapper .title{margin-bottom:2px;color:var(--g-text-dark);font-size:.85rem;font-weight:650;line-height:15px}.gantt-container .popup-wrapper .subtitle{color:var(--g-text-dark);font-size:.8rem;margin-bottom:5px}.gantt-container .popup-wrapper .details{color:var(--g-text-muted);font-size:.7rem}.gantt-container .popup-wrapper .actions{margin-top:10px;margin-left:3px}.gantt-container .popup-wrapper .action-btn{border:none;padding:5px 8px;background-color:var(--g-popup-actions);border-right:1px solid var(--g-text-light)}.gantt-container .popup-wrapper .action-btn:hover{background-color:brightness(97%)}.gantt-container .popup-wrapper .action-btn:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px}.gantt-container .popup-wrapper .action-btn:last-child{border-right:none;border-top-right-radius:4px;border-bottom-right-radius:4px}.gantt-container .grid-header{height:calc(var(--gv-lower-header-height) + var(--gv-upper-header-height) + 10px);background-color:var(--g-header-background);position:sticky;top:0;left:0;border-bottom:1px solid var(--g-row-border-color);z-index:1000}.gantt-container .lower-text,.gantt-container .upper-text{text-anchor:middle}.gantt-container .upper-header{height:var(--gv-upper-header-height)}.gantt-container .lower-header{height:var(--gv-lower-header-height)}.gantt-container .lower-text{font-size:12px;position:absolute;width:calc(var(--gv-column-width) * .8);height:calc(var(--gv-lower-header-height) * .8);margin:0 calc(var(--gv-column-width) * .1);align-content:center;text-align:center;color:var(--g-text-muted)}.gantt-container .upper-text{position:absolute;width:fit-content;font-weight:500;font-size:14px;color:var(--g-text-dark);height:calc(var(--gv-lower-header-height) * .66)}.gantt-container .current-upper{position:sticky;left:0!important;padding-left:17px;background:#fff}.gantt-container .side-header{position:sticky;top:0;right:0;float:right;z-index:1000;line-height:20px;font-weight:400;width:max-content;margin-left:auto;padding-right:10px;padding-top:10px;background:var(--g-header-background);display:flex}.gantt-container .side-header *{transition-property:background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:var(--g-actions-background);border-radius:.5rem;border:none;padding:5px 8px;color:var(--g-text-dark);font-size:14px;letter-spacing:.02em;font-weight:420;box-sizing:content-box;margin-right:5px}.gantt-container .side-header *:last-child{margin-right:0}.gantt-container .side-header *:hover{filter:brightness(97.5%)}.gantt-container .side-header select{width:60px;padding-top:2px;padding-bottom:2px}.gantt-container .side-header select:focus{outline:none}.gantt-container .date-range-highlight{background-color:var(--g-progress-color);border-radius:12px;height:calc(var(--gv-lower-header-height) - 6px);top:calc(var(--gv-upper-header-height) + 5px);position:absolute}.gantt-container .current-highlight{position:absolute;background:var(--g-today-highlight);width:1px;z-index:999}.gantt-container .current-ball-highlight{position:absolute;background:var(--g-today-highlight);z-index:1001;border-radius:50%}.gantt-container .current-date-highlight{background:var(--g-today-highlight);color:var(--g-text-light);border-radius:5px}.gantt-container .holiday-label{position:absolute;top:0;left:0;opacity:0;z-index:1000;background:--g-weekend-label-color;border-radius:5px;padding:2px 5px}.gantt-container .holiday-label.show{opacity:100}.gantt-container .extras{position:sticky;left:0}.gantt-container .extras .adjust{position:absolute;left:8px;top:calc(var(--gv-grid-height) - 60px);background-color:#000000b3;color:#fff;border:none;padding:8px;border-radius:3px}.gantt-container .hide{display:none}.gantt{user-select:none;-webkit-user-select:none;position:absolute}.gantt .grid-background{fill:none}.gantt .grid-row{fill:var(--g-row-color)}.gantt .row-line{stroke:var(--g-border-color)}.gantt .tick{stroke:var(--g-tick-color);stroke-width:.4}.gantt .tick.thick{stroke:var(--g-tick-color-thick);stroke-width:.7}.gantt .arrow{fill:none;stroke:var(--g-arrow-color);stroke-width:1.5}.gantt .bar-wrapper .bar{fill:var(--g-bar-color);stroke:var(--g-bar-border);stroke-width:0;transition:stroke-width .3s ease}.gantt .bar-progress{fill:var(--g-progress-color);border-radius:4px}.gantt .bar-expected-progress{fill:var(--g-expected-progress)}.gantt .bar-invalid{fill:transparent;stroke:var(--g-bar-border);stroke-width:1;stroke-dasharray:5}:is(.gantt .bar-invalid)~.bar-label{fill:var(--g-text-light)}.gantt .bar-label{fill:var(--g-text-dark);dominant-baseline:central;font-family:Helvetica;font-size:13px;font-weight:400}.gantt .bar-label.big{fill:var(--g-text-dark);text-anchor:start}.gantt .handle{fill:var(--g-handle-color);opacity:0;transition:opacity .3s ease}.gantt .handle.active,.gantt .handle.visible{cursor:ew-resize;opacity:1}.gantt .handle.progress{fill:var(--g-text-muted)}.gantt .bar-wrapper{cursor:pointer}.gantt .bar-wrapper .bar{outline:1px solid var(--g-row-border-color);border-radius:3px}.gantt .bar-wrapper:hover .bar{transition:transform .3s ease}.gantt .bar-wrapper:hover .date-range-highlight{display:block} diff --git a/backends/advanced/webui/src/App.tsx b/backends/advanced/webui/src/App.tsx index 6e497dff..6f7f3e72 100644 --- a/backends/advanced/webui/src/App.tsx +++ b/backends/advanced/webui/src/App.tsx @@ -4,8 +4,10 @@ import { ThemeProvider } from './contexts/ThemeContext' import Layout from './components/layout/Layout' import LoginPage from './pages/LoginPage' import Chat from './pages/Chat' -import Conversations from './pages/Conversations' +import ConversationsRouter from './pages/ConversationsRouter' import MemoriesRouter from './pages/MemoriesRouter' +import MemoryDetail from './pages/MemoryDetail' +import TimelineRouter from './pages/TimelineRouter' import Users from './pages/Users' import System from './pages/System' import Upload from './pages/Upload' @@ -31,7 +33,7 @@ function App() { }> - + } /> - + + + } /> + + } /> } /> + + + + } /> diff --git a/backends/advanced/webui/src/components/layout/Layout.tsx b/backends/advanced/webui/src/components/layout/Layout.tsx index 0243d00f..f4caf629 100644 --- a/backends/advanced/webui/src/components/layout/Layout.tsx +++ b/backends/advanced/webui/src/components/layout/Layout.tsx @@ -1,5 +1,5 @@ import { Link, useLocation, Outlet } from 'react-router-dom' -import { Music, MessageSquare, MessageCircle, Brain, Users, Upload, Settings, LogOut, Sun, Moon, Shield, Radio, Layers } from 'lucide-react' +import { Music, MessageSquare, MessageCircle, Brain, Users, Upload, Settings, LogOut, Sun, Moon, Shield, Radio, Layers, Calendar } from 'lucide-react' import { useAuth } from '../../contexts/AuthContext' import { useTheme } from '../../contexts/ThemeContext' @@ -13,6 +13,7 @@ export default function Layout() { { path: '/chat', label: 'Chat', icon: MessageCircle }, { path: '/conversations', label: 'Conversations', icon: MessageSquare }, { path: '/memories', label: 'Memories', icon: Brain }, + { path: '/timeline', label: 'Timeline', icon: Calendar }, { path: '/users', label: 'User Management', icon: Users }, ...(isAdmin ? [ { path: '/upload', label: 'Upload Audio', icon: Upload }, diff --git a/backends/advanced/webui/src/hooks/useAudioRecording.ts b/backends/advanced/webui/src/hooks/useAudioRecording.ts index 5fc2091b..3e303cbc 100644 --- a/backends/advanced/webui/src/hooks/useAudioRecording.ts +++ b/backends/advanced/webui/src/hooks/useAudioRecording.ts @@ -100,8 +100,8 @@ export const useAudioRecording = (): UseAudioRecordingReturn => { const audioContextRef = useRef(null) const analyserRef = useRef(null) const processorRef = useRef(null) - const durationIntervalRef = useRef() - const keepAliveIntervalRef = useRef() + const durationIntervalRef = useRef>() + const keepAliveIntervalRef = useRef>() const audioProcessingStartedRef = useRef(false) const chunkCountRef = useRef(0) // Note: Legacy message queue code removed as it was unused diff --git a/backends/advanced/webui/src/hooks/useD3Zoom.ts b/backends/advanced/webui/src/hooks/useD3Zoom.ts new file mode 100644 index 00000000..8f60b204 --- /dev/null +++ b/backends/advanced/webui/src/hooks/useD3Zoom.ts @@ -0,0 +1,82 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import * as d3 from 'd3' + +interface UseD3ZoomOptions { + onZoom?: (transform: d3.ZoomTransform) => void + scaleExtent?: [number, number] + wheelDelta?: (event: WheelEvent) => number +} + +export function useD3Zoom(options: UseD3ZoomOptions = {}) { + const { + onZoom, + scaleExtent = [0.5, 5], + wheelDelta = (event) => -event.deltaY * 0.002 + } = options + + const svgRef = useRef(null) + const [transform, setTransform] = useState(d3.zoomIdentity) + + const handleZoom = useCallback( + (event: d3.D3ZoomEvent) => { + const t = event.transform + setTransform(t) + onZoom?.(t) + + // Synchronize zoom across all zoomable SVG elements + d3.selectAll('.zoomable').each(function () { + const svg = d3.select(this) + const node = svg.node() + + // Skip the source element + if (!node || node.contains(event.sourceEvent?.target as Element)) { + return + } + + svg.property('__zoom', t) + }) + }, + [onZoom] + ) + + const zoomBehavior = useMemo( + () => + d3.zoom() + .scaleExtent(scaleExtent) + .on('zoom', handleZoom) + .wheelDelta(wheelDelta) + .touchable(() => true) + .filter((event) => { + if (event.type === 'dblclick') return false + if (event.button && event.button !== 0) return false + return true + }), + [handleZoom, scaleExtent, wheelDelta] + ) + + useEffect(() => { + if (!svgRef.current) return + + const svg = d3.select(svgRef.current) + const node = svg.node() + + if (node) { + node.style.touchAction = 'none' + node.style.webkitUserSelect = 'none' + node.style.userSelect = 'none' + } + + svg.call(zoomBehavior as any) + svg.property('__zoom', transform) + + return () => { + svg.on('.zoom', null) + } + }, [zoomBehavior, transform]) + + return { + svgRef, + transform, + zoomBehavior + } +} diff --git a/backends/advanced/webui/src/hooks/useSimpleAudioRecording.ts b/backends/advanced/webui/src/hooks/useSimpleAudioRecording.ts index 268544c7..e0a1badc 100644 --- a/backends/advanced/webui/src/hooks/useSimpleAudioRecording.ts +++ b/backends/advanced/webui/src/hooks/useSimpleAudioRecording.ts @@ -58,8 +58,8 @@ export const useSimpleAudioRecording = (): SimpleAudioRecordingReturn => { const audioContextRef = useRef(null) const analyserRef = useRef(null) const processorRef = useRef(null) - const durationIntervalRef = useRef() - const keepAliveIntervalRef = useRef() + const durationIntervalRef = useRef>() + const keepAliveIntervalRef = useRef>() const chunkCountRef = useRef(0) const audioProcessingStartedRef = useRef(false) diff --git a/backends/advanced/webui/src/pages/ConversationsRouter.tsx b/backends/advanced/webui/src/pages/ConversationsRouter.tsx new file mode 100644 index 00000000..c7e6e95c --- /dev/null +++ b/backends/advanced/webui/src/pages/ConversationsRouter.tsx @@ -0,0 +1,48 @@ +import { useState } from 'react' +import Conversations from './Conversations' +import ConversationsTimeline from './ConversationsTimeline' + +export default function ConversationsRouter() { + const [activeTab, setActiveTab] = useState<'classic' | 'timeline'>('classic') + + return ( +
+ {/* Tab Navigation */} +
+ +
+ + {/* Content */} + {activeTab === 'classic' ? ( + + ) : ( + + )} +
+ ) +} diff --git a/backends/advanced/webui/src/pages/ConversationsTimeline.tsx b/backends/advanced/webui/src/pages/ConversationsTimeline.tsx new file mode 100644 index 00000000..5c3f748f --- /dev/null +++ b/backends/advanced/webui/src/pages/ConversationsTimeline.tsx @@ -0,0 +1,321 @@ +import { useState, useEffect } from 'react' +import { MessageSquare, RefreshCw, User, Clock, ChevronDown, ChevronUp } from 'lucide-react' +import { VerticalTimeline, VerticalTimelineElement } from 'react-vertical-timeline-component' +import 'react-vertical-timeline-component/style.min.css' +import { conversationsApi } from '../services/api' + +interface Conversation { + conversation_id?: string + audio_uuid: string + title?: string + summary?: string + detailed_summary?: string + created_at?: string + client_id: string + segment_count?: number + memory_count?: number + audio_path?: string + cropped_audio_path?: string + duration_seconds?: number + has_memory?: boolean + transcript?: string + segments?: Array<{ + text: string + speaker: string + start: number + end: number + confidence?: number + }> + active_transcript_version?: string + active_memory_version?: string + transcript_version_count?: number + memory_version_count?: number + deleted?: boolean + deletion_reason?: string + deleted_at?: string +} + +interface ConversationCardProps { + conversation: Conversation + formatDuration: (seconds: number) => string +} + +function ConversationCard({ conversation, formatDuration }: ConversationCardProps) { + const [isExpanded, setIsExpanded] = useState(false) + + return ( +
+ {/* Card Header - Always visible */} +
setIsExpanded(!isExpanded)} + > +
+

+ {conversation.title || 'Conversation'} +

+ {isExpanded ? ( + + ) : ( + + )} +
+ + {conversation.summary && ( +

+ {conversation.summary} +

+ )} + +
+ + + {conversation.client_id} + + {conversation.segment_count !== undefined && ( + + {conversation.segment_count} segments + + )} + {conversation.memory_count !== undefined && conversation.memory_count > 0 && ( + + {conversation.memory_count} memories + + )} + {conversation.duration_seconds && ( + + + {formatDuration(conversation.duration_seconds)} + + )} + {conversation.deleted && ( + + Failed: {conversation.deletion_reason || 'Unknown'} + + )} +
+
+ + {/* Expanded Details */} + {isExpanded && ( +
+ {/* Detailed Summary */} + {conversation.detailed_summary && ( +
+

Detailed Summary

+

{conversation.detailed_summary}

+
+ )} + + {/* Transcript */} + {conversation.transcript && ( +
+

Transcript

+
+ {conversation.transcript} +
+
+ )} + + {/* Segments */} + {conversation.segments && conversation.segments.length > 0 && ( +
+

Segments ({conversation.segments.length})

+
+ {conversation.segments.map((segment, idx) => ( +
+
+ {segment.speaker} + + {Math.floor(segment.start)}s - {Math.floor(segment.end)}s + +
+

{segment.text}

+ {segment.confidence && ( + + Confidence: {(segment.confidence * 100).toFixed(1)}% + + )} +
+ ))} +
+
+ )} + + {/* Metadata */} +
+ {conversation.conversation_id && ( +
+ ID:{' '} + {conversation.conversation_id.slice(0, 8)}... +
+ )} + {conversation.audio_uuid && ( +
+ Audio UUID:{' '} + {conversation.audio_uuid.slice(0, 8)}... +
+ )} + {conversation.active_transcript_version && ( +
+ Transcript Version:{' '} + {conversation.active_transcript_version} +
+ )} + {conversation.transcript_version_count && ( +
+ Total Versions:{' '} + {conversation.transcript_version_count} +
+ )} +
+ + {/* Audio Paths */} + {(conversation.audio_path || conversation.cropped_audio_path) && ( +
+ {conversation.audio_path && ( +
+ Audio:{' '} + {conversation.audio_path} +
+ )} + {conversation.cropped_audio_path && ( +
+ Cropped:{' '} + {conversation.cropped_audio_path} +
+ )} +
+ )} +
+ )} +
+ ) +} + +export default function ConversationsTimeline() { + const [conversations, setConversations] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const loadConversations = async () => { + try { + setLoading(true) + const response = await conversationsApi.getAll() + const conversationsList = response.data.conversations || [] + setConversations(conversationsList) + setError(null) + } catch (err: any) { + setError(err.message || 'Failed to load conversations') + } finally { + setLoading(false) + } + } + + useEffect(() => { + loadConversations() + }, []) + + const formatDate = (timestamp: number | string): Date => { + if (typeof timestamp === 'string') { + const isoString = timestamp.endsWith('Z') || timestamp.includes('+') || timestamp.includes('T') && timestamp.split('T')[1].includes('-') + ? timestamp + : timestamp + 'Z' + return new Date(isoString) + } + if (timestamp === 0) { + return new Date() + } + return new Date(timestamp * 1000) + } + + const formatDuration = (seconds: number) => { + const minutes = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${minutes}:${secs.toString().padStart(2, '0')}` + } + + if (loading) { + return ( +
+
+ Loading conversations... +
+ ) + } + + if (error) { + return ( +
+
{error}
+ +
+ ) + } + + return ( +
+ {/* Header */} +
+
+ +

+ Conversations Timeline +

+
+ +
+ + {/* Timeline */} + {conversations.length === 0 ? ( +
+ +

No conversations found

+
+ ) : ( + + {conversations.map((conv) => { + const date = formatDate(conv.created_at || '') + + return ( + } + contentStyle={{ + background: conv.deleted ? '#fee2e2' : '#fff', + color: '#1f2937', + boxShadow: '0 3px 0 #ddd' + }} + contentArrowStyle={{ borderRight: '7px solid #fff' }} + > + + + ) + })} + + )} +
+ ) +} diff --git a/backends/advanced/webui/src/pages/FrappeGanttTimeline.tsx b/backends/advanced/webui/src/pages/FrappeGanttTimeline.tsx new file mode 100644 index 00000000..d8da0aed --- /dev/null +++ b/backends/advanced/webui/src/pages/FrappeGanttTimeline.tsx @@ -0,0 +1,707 @@ +import { useState, useEffect, useRef } from 'react' +import { Calendar, RefreshCw, AlertCircle, ZoomIn, ZoomOut } from 'lucide-react' +import Gantt from 'frappe-gantt' +import { memoriesApi } from '../services/api' +import { useAuth } from '../contexts/AuthContext' + +interface TimeRange { + start: string + end: string + name?: string +} + +interface MemoryWithTimeRange { + id: string + content: string + created_at: string + metadata?: { + name?: string + timeRanges?: TimeRange[] + isPerson?: boolean + isEvent?: boolean + isPlace?: boolean + } +} + +interface GanttTask { + id: string + name: string + start: string + end: string + progress: number + custom_class?: string +} + +export default function FrappeGanttTimeline() { + const [memories, setMemories] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [useDemoData, setUseDemoData] = useState(false) + const [currentViewMode, setCurrentViewMode] = useState('Week') + const [zoomScale, setZoomScale] = useState(1) // CSS transform scale: 0.5 = 50%, 1 = 100%, 2 = 200% + const ganttContainerRef = useRef(null) + const ganttInstance = useRef(null) + const scrollContainerRef = useRef(null) + const isDragging = useRef(false) + const startX = useRef(0) + const scrollLeft = useRef(0) + const { user } = useAuth() + + // Demo data for testing the Timeline visualization - spans multiple years + const getDemoMemories = (): MemoryWithTimeRange[] => { + return [ + { + id: 'demo-graduation', + content: 'College graduation ceremony and celebration dinner with family.', + created_at: '2024-05-20T14:00:00', + metadata: { + name: 'College Graduation', + isEvent: true, + timeRanges: [ + { + name: 'Graduation Ceremony', + start: '2024-05-20T14:00:00', + end: '2024-05-20T17:00:00' + }, + { + name: 'Celebration Dinner', + start: '2024-05-20T19:00:00', + end: '2024-05-20T22:00:00' + } + ] + } + }, + { + id: 'demo-wedding', + content: "Sarah and Tom's wedding was a beautiful celebration. The ceremony started at 3 PM, followed by a reception that lasted until midnight.", + created_at: '2025-06-15T15:00:00', + metadata: { + name: "Sarah & Tom's Wedding", + isEvent: true, + timeRanges: [ + { + name: 'Wedding Ceremony', + start: '2025-06-15T15:00:00', + end: '2025-06-15T16:30:00' + }, + { + name: 'Reception', + start: '2025-06-15T18:00:00', + end: '2025-06-16T00:00:00' + } + ] + } + }, + { + id: 'demo-conference', + content: 'Tech conference with keynote presentations and networking sessions throughout the day.', + created_at: '2025-09-20T09:00:00', + metadata: { + name: 'Tech Conference 2025', + isEvent: true, + timeRanges: [ + { + name: 'Morning Keynote', + start: '2025-09-20T09:00:00', + end: '2025-09-20T11:00:00' + }, + { + name: 'Workshops', + start: '2025-09-20T13:00:00', + end: '2025-09-20T17:00:00' + } + ] + } + }, + { + id: 'demo-vacation', + content: 'Week-long vacation at the beach house with family.', + created_at: '2026-07-01T14:00:00', + metadata: { + name: 'Summer Vacation 2026', + isPlace: true, + timeRanges: [ + { + name: 'Beach House Stay', + start: '2026-07-01T14:00:00', + end: '2026-07-07T12:00:00' + } + ] + } + }, + { + id: 'demo-reunion', + content: 'Family reunion at the old homestead with extended family gathering.', + created_at: '2026-12-25T12:00:00', + metadata: { + name: 'Family Reunion', + isEvent: true, + timeRanges: [ + { + name: 'Christmas Gathering', + start: '2026-12-25T12:00:00', + end: '2026-12-25T20:00:00' + } + ] + } + } + ] + } + + const loadMemories = async () => { + if (!user?.id) return + + try { + setLoading(true) + setError(null) + const response = await memoriesApi.getAll(user.id) + + // Extract memories from response + const memoriesData = response.data.memories || response.data || [] + + // Filter memories that have timeRanges + const memoriesWithTime = memoriesData.filter((m: MemoryWithTimeRange) => + m.metadata?.timeRanges && m.metadata.timeRanges.length > 0 + ) + + console.log('๐Ÿ“… Timeline: Total memories:', memoriesData.length) + console.log('๐Ÿ“… Timeline: Memories with timeRanges:', memoriesWithTime.length) + if (memoriesWithTime.length > 0) { + console.log('๐Ÿ“… Timeline: First memory with timeRange:', memoriesWithTime[0]) + } + + setMemories(memoriesWithTime) + } catch (err: any) { + console.error('โŒ Timeline loading error:', err) + setError(err.message || 'Failed to load timeline data') + } finally { + setLoading(false) + } + } + + const convertMemoriesToGanttTasks = (memories: MemoryWithTimeRange[]): GanttTask[] => { + const tasks: GanttTask[] = [] + + memories.forEach((memory) => { + const timeRanges = memory.metadata?.timeRanges || [] + + timeRanges.forEach((range, index) => { + // Get the task name from the range name, memory metadata name, or content preview + const taskName = range.name || + memory.metadata?.name || + memory.content.substring(0, 50) + (memory.content.length > 50 ? '...' : '') + + // Determine custom class based on memory type + let customClass = 'default' + if (memory.metadata?.isEvent) customClass = 'event' + else if (memory.metadata?.isPerson) customClass = 'person' + else if (memory.metadata?.isPlace) customClass = 'place' + + tasks.push({ + id: `${memory.id}-${index}`, + name: taskName, + start: range.start, + end: range.end, + progress: 100, // All memories are completed events + custom_class: customClass + }) + }) + }) + + return tasks + } + + useEffect(() => { + if (!useDemoData) { + loadMemories() + } else { + setMemories(getDemoMemories()) + } + }, [user?.id, useDemoData]) + + useEffect(() => { + const displayMemories = useDemoData ? getDemoMemories() : memories + + if (!ganttContainerRef.current || displayMemories.length === 0) { + return + } + + // Convert memories to Gantt tasks + const tasks = convertMemoriesToGanttTasks(displayMemories) + + if (tasks.length === 0) { + return + } + + console.log('๐Ÿ“Š Creating Gantt chart with tasks:', tasks) + + try { + // Clear existing Gantt instance + if (ganttInstance.current) { + ganttContainerRef.current.innerHTML = '' + } + + // Create new Gantt instance with type assertion for custom_popup_html + ganttInstance.current = new Gantt(ganttContainerRef.current, tasks, { + view_mode: currentViewMode, + bar_height: 30, + bar_corner_radius: 3, + arrow_curve: 5, + padding: 18, + date_format: 'YYYY-MM-DD', + language: 'en', + custom_popup_html: (task: any) => { + const memory = displayMemories.find(m => task.id.startsWith(m.id)) + const startDate = new Date(task._start) + const endDate = new Date(task._end) + const formatOptions: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + } + return ` + + ` + } + } as any) + + console.log('โœ… Gantt chart created successfully') + + // Add year labels to the timeline header + setTimeout(() => { + try { + const container = ganttContainerRef.current?.querySelector('.gantt-container') + if (!container) return + + // Find all unique years from tasks + const years = new Set() + tasks.forEach(task => { + const startYear = new Date(task.start).getFullYear() + const endYear = new Date(task.end).getFullYear() + years.add(startYear) + if (startYear !== endYear) years.add(endYear) + }) + + const sortedYears = Array.from(years).sort() + if (sortedYears.length <= 1) return // No need for year labels if single year + + // Get the upper header div element (HTML, not SVG) + const upperHeader = container.querySelector('.upper-header') + if (!upperHeader) return + + // Add year labels as HTML divs in a simple row at the top + sortedYears.forEach((year, index) => { + const yearLabel = document.createElement('div') + yearLabel.className = 'year-label' + yearLabel.textContent = String(year) + yearLabel.style.position = 'absolute' + yearLabel.style.left = `${20 + (index * 70)}px` // Simple horizontal spacing + yearLabel.style.top = '2px' + yearLabel.style.fontSize = '18px' + yearLabel.style.fontWeight = '700' + yearLabel.style.color = '#2563eb' // Blue color + yearLabel.style.padding = '2px 8px' + yearLabel.style.backgroundColor = '#eff6ff' + yearLabel.style.borderRadius = '4px' + yearLabel.style.zIndex = '10' + + upperHeader.appendChild(yearLabel) + }) + + } catch (error) { + console.warn('Failed to add year labels:', error) + } + }, 150) // Small delay to ensure DOM is fully rendered + } catch (err) { + console.error('โŒ Error creating Gantt chart:', err) + setError('Failed to create timeline visualization') + } + + return () => { + if (ganttInstance.current && ganttContainerRef.current) { + ganttContainerRef.current.innerHTML = '' + ganttInstance.current = null + } + } + }, [memories, useDemoData, currentViewMode]) + + // Drag-to-scroll functionality + useEffect(() => { + const container = scrollContainerRef.current + if (!container) return + + const handleMouseDown = (e: MouseEvent) => { + // Only start drag if not clicking on interactive elements + const target = e.target as HTMLElement + if (target.closest('.bar-wrapper') || target.closest('button')) { + return + } + + isDragging.current = true + startX.current = e.pageX + scrollLeft.current = container.scrollLeft + container.style.cursor = 'grabbing' + e.preventDefault() + } + + const handleMouseLeave = () => { + isDragging.current = false + container.style.cursor = 'grab' + } + + const handleMouseUp = () => { + isDragging.current = false + container.style.cursor = 'grab' + } + + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging.current) return + e.preventDefault() + const x = e.pageX + const walk = (x - startX.current) * 1.5 // Scroll speed multiplier + container.scrollLeft = scrollLeft.current - walk + } + + // Add event listeners with capture phase for better control + container.addEventListener('mousedown', handleMouseDown, true) + container.addEventListener('mouseleave', handleMouseLeave) + container.addEventListener('mouseup', handleMouseUp) + container.addEventListener('mousemove', handleMouseMove) + + return () => { + container.removeEventListener('mousedown', handleMouseDown, true) + container.removeEventListener('mouseleave', handleMouseLeave) + container.removeEventListener('mouseup', handleMouseUp) + container.removeEventListener('mousemove', handleMouseMove) + } + }, []) + + // Mousewheel zoom functionality + useEffect(() => { + const container = scrollContainerRef.current + if (!container) return + + const viewModeOrder = ['Quarter Day', 'Half Day', 'Day', 'Week', 'Month'] + + const handleWheel = (e: WheelEvent) => { + // Only zoom when Ctrl or Cmd is pressed + if (e.ctrlKey || e.metaKey) { + e.preventDefault() + e.stopPropagation() + + const currentIndex = viewModeOrder.indexOf(currentViewMode) + + if (e.deltaY < 0) { + // Zoom in (scroll up = more detailed view) + if (currentIndex > 0) { + setCurrentViewMode(viewModeOrder[currentIndex - 1]) + } + } else if (e.deltaY > 0) { + // Zoom out (scroll down = less detailed view) + if (currentIndex < viewModeOrder.length - 1) { + setCurrentViewMode(viewModeOrder[currentIndex + 1]) + } + } + } + // If no modifier keys, let the browser handle normal horizontal scrolling + } + + container.addEventListener('wheel', handleWheel, { passive: false }) + + return () => { + container.removeEventListener('wheel', handleWheel) + } + }, [currentViewMode]) + + const viewModes = [ + { value: 'Quarter Day', label: 'Quarter Day' }, + { value: 'Half Day', label: 'Half Day' }, + { value: 'Day', label: 'Day' }, + { value: 'Week', label: 'Week' }, + { value: 'Month', label: 'Month' } + ] + + const changeViewMode = (mode: string) => { + setCurrentViewMode(mode) + } + + const zoomIn = () => { + setZoomScale(prev => { + const newScale = Math.min(prev + 0.25, 3) // Max 300% + // Store scroll position ratio before zoom + if (scrollContainerRef.current) { + const container = scrollContainerRef.current + const scrollRatio = (container.scrollLeft + container.clientWidth / 2) / container.scrollWidth + + // After state update, restore relative scroll position + setTimeout(() => { + if (scrollContainerRef.current) { + const newScrollLeft = scrollRatio * scrollContainerRef.current.scrollWidth - container.clientWidth / 2 + scrollContainerRef.current.scrollLeft = newScrollLeft + } + }, 0) + } + return newScale + }) + } + + const zoomOut = () => { + setZoomScale(prev => { + const newScale = Math.max(prev - 0.25, 0.5) // Min 50% + // Store scroll position ratio before zoom + if (scrollContainerRef.current) { + const container = scrollContainerRef.current + const scrollRatio = (container.scrollLeft + container.clientWidth / 2) / container.scrollWidth + + // After state update, restore relative scroll position + setTimeout(() => { + if (scrollContainerRef.current) { + const newScrollLeft = scrollRatio * scrollContainerRef.current.scrollWidth - container.clientWidth / 2 + scrollContainerRef.current.scrollLeft = newScrollLeft + } + }, 0) + } + return newScale + }) + } + + if (loading) { + return ( +
+
+

Timeline

+
+
+
+ + Loading timeline data... +
+
+
+ ) + } + + if (error) { + return ( +
+
+

Timeline

+
+
+
+ + {error} +
+
+
+ ) + } + + if (memories.length === 0 && !useDemoData) { + return ( +
+
+

Timeline

+
+ + +
+
+
+ +
+

No Timeline Events

+

+ No memories with time information found. Create memories with dates and times to see them on the timeline. +

+

+ Click "Show Demo" to see how the timeline works with sample data. +

+
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+

Timeline (Frappe Gantt) {useDemoData && (Demo Mode)}

+

+ {useDemoData ? getDemoMemories().length : memories.length} {(useDemoData ? getDemoMemories().length : memories.length) === 1 ? 'event' : 'events'} with time information +

+
+
+ {/* Demo mode toggle */} + {useDemoData ? ( + + ) : ( + + )} + {/* Zoom controls */} +
+ +
+ {Math.round(zoomScale * 100)}% +
+ +
+ {/* View mode selector */} +
+ + +
+ +
+
+ + {/* Gantt Chart Container */} +
+ {/* Scrollable Gantt Chart */} +
+
+
+ + {/* Instructions - Fixed, not scrolling */} +
+ ๐Ÿ’ก Drag to scroll horizontally + ๐Ÿ” Hold Ctrl/Cmd + Scroll to zoom in/out +
+ + {/* Legend - Fixed, not scrolling */} +
+
+
+ Event +
+
+
+ Person +
+
+
+ Place +
+
+
+ + {/* Add custom styles for Gantt chart colors */} + +
+ ) +} diff --git a/backends/advanced/webui/src/pages/MyceliaTimeline.tsx b/backends/advanced/webui/src/pages/MyceliaTimeline.tsx new file mode 100644 index 00000000..48a4a24a --- /dev/null +++ b/backends/advanced/webui/src/pages/MyceliaTimeline.tsx @@ -0,0 +1,441 @@ +import { useState, useEffect, useRef } from 'react' +import { Calendar, RefreshCw, AlertCircle } from 'lucide-react' +import { useNavigate } from 'react-router-dom' +import * as d3 from 'd3' +import { memoriesApi } from '../services/api' +import { useAuth } from '../contexts/AuthContext' + +interface TimeRange { + start: string + end: string + name?: string +} + +interface MemoryWithTimeRange { + id: string + content: string + created_at: string + metadata?: { + name?: string + timeRanges?: TimeRange[] + isPerson?: boolean + isEvent?: boolean + isPlace?: boolean + } +} + +interface TimelineTask { + id: string + name: string + start: Date + end: Date + color: string + type: 'event' | 'person' | 'place' +} + +export default function MyceliaTimeline() { + const [memories, setMemories] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [useDemoData, setUseDemoData] = useState(false) + const svgRef = useRef(null) + const containerRef = useRef(null) + const tooltipRef = useRef(null) + const [dimensions, setDimensions] = useState({ width: 1000, height: 400 }) + const { user } = useAuth() + const navigate = useNavigate() + + // Demo data + const getDemoMemories = (): MemoryWithTimeRange[] => { + return [ + { + id: 'demo-wedding', + content: "Sarah and Tom's wedding ceremony and reception", + created_at: '2025-12-07T15:00:00', + metadata: { + name: "Wedding", + isEvent: true, + timeRanges: [ + { + name: 'Ceremony', + start: '2025-12-07T15:00:00', + end: '2025-12-07T16:30:00' + }, + { + name: 'Reception', + start: '2025-12-07T18:00:00', + end: '2025-12-07T23:00:00' + } + ] + } + }, + { + id: 'demo-conference', + content: 'Tech conference with keynote and workshops', + created_at: '2026-01-15T09:00:00', + metadata: { + name: 'Tech Conference', + isEvent: true, + timeRanges: [ + { + name: 'Keynote', + start: '2026-01-15T09:00:00', + end: '2026-01-15T11:00:00' + } + ] + } + } + ] + } + + const loadMemories = async () => { + if (!user?.id) return + + try { + setLoading(true) + setError(null) + const response = await memoriesApi.getAll(user.id) + const memoriesData = response.data.memories || response.data || [] + const memoriesWithTime = memoriesData.filter((m: MemoryWithTimeRange) => + m.metadata?.timeRanges && m.metadata.timeRanges.length > 0 + ) + setMemories(memoriesWithTime) + } catch (err: any) { + setError(err.message || 'Failed to load timeline data') + } finally { + setLoading(false) + } + } + + const convertToTasks = (memories: MemoryWithTimeRange[]): TimelineTask[] => { + const tasks: TimelineTask[] = [] + memories.forEach((memory) => { + const timeRanges = memory.metadata?.timeRanges || [] + timeRanges.forEach((range, index) => { + let type: 'event' | 'person' | 'place' = 'event' + let color = '#3b82f6' + + if (memory.metadata?.isEvent) { + type = 'event' + color = '#3b82f6' + } else if (memory.metadata?.isPerson) { + type = 'person' + color = '#10b981' + } else if (memory.metadata?.isPlace) { + type = 'place' + color = '#f59e0b' + } + + tasks.push({ + id: `${memory.id}-${index}`, + name: range.name || memory.metadata?.name || memory.content.substring(0, 30), + start: new Date(range.start), + end: new Date(range.end), + color, + type + }) + }) + }) + return tasks + } + + useEffect(() => { + if (!useDemoData) { + loadMemories() + } else { + setMemories(getDemoMemories()) + } + }, [user?.id, useDemoData]) + + // Handle container resize + useEffect(() => { + if (!containerRef.current) return + const resizeObserver = new ResizeObserver(([entry]) => { + setDimensions({ + width: entry.contentRect.width, + height: 400 + }) + }) + resizeObserver.observe(containerRef.current) + return () => resizeObserver.disconnect() + }, []) + + // D3 visualization + useEffect(() => { + if (!svgRef.current || memories.length === 0) return + + const tasks = convertToTasks(useDemoData ? getDemoMemories() : memories) + if (tasks.length === 0) return + + const svg = d3.select(svgRef.current) + svg.selectAll('*').remove() + + const margin = { top: 60, right: 40, bottom: 60, left: 150 } + const width = dimensions.width - margin.left - margin.right + const height = dimensions.height - margin.top - margin.bottom + + // Find time range + const allDates = tasks.flatMap(t => [t.start, t.end]) + const minDate = d3.min(allDates)! + const maxDate = d3.max(allDates)! + + // Create scales + const xScale = d3.scaleTime() + .domain([minDate, maxDate]) + .range([0, width]) + + const yScale = d3.scaleBand() + .domain(tasks.map(t => t.id)) + .range([0, height]) + .padding(0.3) + + // Create main group + const g = svg.append('g') + .attr('transform', `translate(${margin.left},${margin.top})`) + .attr('class', 'zoomable') + + // Add axes + const xAxis = d3.axisBottom(xScale) + .ticks(6) + .tickFormat(d3.timeFormat('%b %d, %Y') as any) + + g.append('g') + .attr('class', 'x-axis') + .attr('transform', `translate(0,${height})`) + .call(xAxis) + .selectAll('text') + .style('fill', 'currentColor') + + // Add task bars + const bars = g.append('g') + .attr('class', 'bars') + .selectAll('rect') + .data(tasks) + .enter() + + // Bar background with click and hover + bars.append('rect') + .attr('x', d => xScale(d.start)) + .attr('y', d => yScale(d.id)!) + .attr('width', d => Math.max(2, xScale(d.end) - xScale(d.start))) + .attr('height', yScale.bandwidth()) + .attr('fill', d => d.color) + .attr('rx', 4) + .style('opacity', 0.8) + .style('cursor', 'pointer') + .on('mouseover', function(event, d) { + d3.select(this).style('opacity', 1) + + // Show tooltip + if (tooltipRef.current) { + const tooltip = d3.select(tooltipRef.current) + const startDate = d.start.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + const endDate = d.end.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + + tooltip + .style('opacity', 1) + .style('left', `${event.pageX + 10}px`) + .style('top', `${event.pageY - 10}px`) + .html(` +
${d.name}
+
+
Start: ${startDate}
+
End: ${endDate}
+
Click to view memory
+
+ `) + } + }) + .on('mouseout', function() { + d3.select(this).style('opacity', 0.8) + + // Hide tooltip + if (tooltipRef.current) { + d3.select(tooltipRef.current).style('opacity', 0) + } + }) + .on('click', function(event, d) { + event.stopPropagation() + // Extract memory ID from task ID (format: "memory-id-rangeIndex") + const memoryId = d.id.split('-').slice(0, -1).join('-') + navigate(`/memories/${memoryId}`) + }) + + // Add labels + g.append('g') + .attr('class', 'labels') + .selectAll('text') + .data(tasks) + .enter() + .append('text') + .attr('x', -10) + .attr('y', d => yScale(d.id)! + yScale.bandwidth() / 2) + .attr('dy', '0.35em') + .attr('text-anchor', 'end') + .text(d => d.name) + .style('fill', 'currentColor') + .style('font-size', '12px') + + // Zoom behavior + const zoom = d3.zoom() + .scaleExtent([0.5, 5]) + .on('zoom', (event) => { + const transform = event.transform + + // Update x scale + const newXScale = transform.rescaleX(xScale) + + // Update axis + g.select('.x-axis').call( + d3.axisBottom(newXScale) + .ticks(6) + .tickFormat(d3.timeFormat('%b %d, %Y') as any) as any + ) + + // Update bars + g.selectAll('.bars rect') + .attr('x', d => newXScale(d.start)) + .attr('width', d => Math.max(2, newXScale(d.end) - newXScale(d.start))) + }) + + svg.call(zoom as any) + + }, [memories, dimensions, useDemoData]) + + if (loading) { + return ( +
+
+
+ + Loading timeline data... +
+
+
+ ) + } + + if (error) { + return ( +
+
+
+ + {error} +
+
+
+ ) + } + + return ( +
+ {/* Tooltip */} +
+ + {/* Header */} +
+
+

+ + Timeline (Mycelia D3) +

+

+ Interactive D3-based timeline with smooth pan and zoom โ€ข Click events to view details +

+
+
+ {useDemoData ? ( + + ) : ( + + )} + +
+
+ + {/* Timeline */} + {memories.length === 0 && !useDemoData ? ( +
+ +
+

No Timeline Events

+

+ No memories with time information found. Try the demo to see how it works. +

+
+
+ ) : ( +
+
+ +
+ +
+ ๐Ÿ’ก Scroll to zoom, drag to pan + ๐Ÿ–ฑ๏ธ Click bars to view memory details + ๐Ÿ‘† Hover for info +
+ +
+
+
+ Event +
+
+
+ Person +
+
+
+ Place +
+
+
+ )} +
+ ) +} diff --git a/backends/advanced/webui/src/pages/ReactGanttTimeline.tsx b/backends/advanced/webui/src/pages/ReactGanttTimeline.tsx new file mode 100644 index 00000000..e1bc127f --- /dev/null +++ b/backends/advanced/webui/src/pages/ReactGanttTimeline.tsx @@ -0,0 +1,359 @@ +import { useState, useEffect } from 'react' +import { Calendar, RefreshCw, AlertCircle, ZoomIn, ZoomOut } from 'lucide-react' +import Timeline from 'react-gantt-timeline' +import { memoriesApi } from '../services/api' +import { useAuth } from '../contexts/AuthContext' + +interface TimeRange { + start: string + end: string + name?: string +} + +interface MemoryWithTimeRange { + id: string + content: string + created_at: string + metadata?: { + name?: string + timeRanges?: TimeRange[] + isPerson?: boolean + isEvent?: boolean + isPlace?: boolean + } +} + +interface ReactGanttTask { + id: string + name: string + start: Date + end: Date + color?: string +} + +export default function ReactGanttTimeline() { + const [memories, setMemories] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [useDemoData, setUseDemoData] = useState(false) + const [zoomLevel, setZoomLevel] = useState(1) // 0.5 = 50%, 1 = 100%, 2 = 200% + const { user } = useAuth() + + const handleZoomIn = () => { + setZoomLevel(prev => Math.min(prev + 0.25, 3)) // Max 300% + } + + const handleZoomOut = () => { + setZoomLevel(prev => Math.max(prev - 0.25, 0.5)) // Min 50% + } + + // Demo data for testing the Timeline visualization - spans multiple years + const getDemoMemories = (): MemoryWithTimeRange[] => { + return [ + { + id: 'demo-graduation', + content: 'College graduation ceremony and celebration dinner with family.', + created_at: '2024-05-20T14:00:00', + metadata: { + name: 'College Graduation', + isEvent: true, + timeRanges: [ + { + name: 'Graduation Ceremony', + start: '2024-05-20T14:00:00', + end: '2024-05-20T17:00:00' + }, + { + name: 'Celebration Dinner', + start: '2024-05-20T18:00:00', + end: '2024-05-20T21:00:00' + } + ] + } + }, + { + id: 'demo-vacation', + content: 'Summer vacation in Hawaii with family. Visited beaches, hiked Diamond Head, attended a luau.', + created_at: '2024-07-10T08:00:00', + metadata: { + name: 'Hawaii Vacation', + isEvent: true, + timeRanges: [ + { + name: 'Hawaii Trip', + start: '2024-07-10T08:00:00', + end: '2024-07-17T20:00:00' + } + ] + } + }, + { + id: 'demo-marathon', + content: 'Completed first marathon in Boston. Training started 6 months ago.', + created_at: '2025-04-15T06:00:00', + metadata: { + name: 'Boston Marathon', + isEvent: true, + timeRanges: [ + { + name: 'Marathon Race', + start: '2025-04-15T06:00:00', + end: '2025-04-15T11:30:00' + } + ] + } + }, + { + id: 'demo-wedding', + content: "Sarah and Tom's wedding was a beautiful celebration. The ceremony started at 3 PM, followed by a reception.", + created_at: '2025-06-15T15:00:00', + metadata: { + name: "Sarah & Tom's Wedding", + isEvent: true, + timeRanges: [ + { + name: 'Wedding Ceremony', + start: '2025-06-15T15:00:00', + end: '2025-06-15T16:30:00' + }, + { + name: 'Reception', + start: '2025-06-15T18:00:00', + end: '2025-06-16T00:00:00' + } + ] + } + }, + { + id: 'demo-conference', + content: 'Tech conference in San Francisco. Attended keynotes, workshops, and networking events.', + created_at: '2026-03-10T09:00:00', + metadata: { + name: 'Tech Conference 2026', + isEvent: true, + timeRanges: [ + { + name: 'Conference', + start: '2026-03-10T09:00:00', + end: '2026-03-13T18:00:00' + } + ] + } + } + ] + } + + const fetchMemoriesWithTimeRanges = async () => { + setLoading(true) + setError(null) + try { + const response = await memoriesApi.getAll() + + // Extract memories from response + const memoriesData = response.data.memories || response.data || [] + + const memoriesWithTimeRanges = memoriesData.filter( + (memory: MemoryWithTimeRange) => + memory.metadata?.timeRanges && + memory.metadata.timeRanges.length > 0 + ) + + if (memoriesWithTimeRanges.length === 0) { + setUseDemoData(true) + setMemories(getDemoMemories()) + setError('No memories with time ranges found. Showing demo data.') + } else { + setMemories(memoriesWithTimeRanges) + setUseDemoData(false) + } + } catch (err) { + console.error('Failed to fetch memories:', err) + setError('Failed to load memories. Showing demo data.') + setUseDemoData(true) + setMemories(getDemoMemories()) + } finally { + setLoading(false) + } + } + + useEffect(() => { + if (user) { + fetchMemoriesWithTimeRanges() + } + }, [user]) + + const handleRefresh = () => { + fetchMemoriesWithTimeRanges() + } + + const handleToggleDemoData = () => { + if (useDemoData) { + fetchMemoriesWithTimeRanges() + } else { + setMemories(getDemoMemories()) + setUseDemoData(true) + } + } + + // Convert memories to react-gantt-timeline format + const convertToReactGanttFormat = (memories: MemoryWithTimeRange[]): ReactGanttTask[] => { + const tasks: ReactGanttTask[] = [] + + memories.forEach((memory) => { + const timeRanges = memory.metadata?.timeRanges || [] + const isEvent = memory.metadata?.isEvent + const isPerson = memory.metadata?.isPerson + const isPlace = memory.metadata?.isPlace + + let color = '#3b82f6' // default blue + if (isEvent) color = '#3b82f6' // blue + else if (isPerson) color = '#10b981' // green + else if (isPlace) color = '#f59e0b' // amber + + timeRanges.forEach((range, index) => { + tasks.push({ + id: `${memory.id}-${index}`, + name: range.name || memory.metadata?.name || memory.content.substring(0, 30), + start: new Date(range.start), + end: new Date(range.end), + color: color + }) + }) + }) + + return tasks + } + + const tasks = convertToReactGanttFormat(memories) + + const data = tasks.map((task) => ({ + id: task.id, + name: task.name, + start: task.start, + end: task.end, + color: task.color + })) + + return ( +
+
+
+

+ + Timeline (React Gantt) +

+

+ Visualize your memories on an interactive timeline using react-gantt-timeline +

+
+
+ {/* Zoom controls */} +
+ +
+ {Math.round(zoomLevel * 100)}% +
+ +
+ + +
+
+ + {error && ( +
+ + {error} +
+ )} + + {loading ? ( +
+ +
+ ) : memories.length === 0 ? ( +
+ +

+ No Timeline Data +

+

+ No memories with time ranges found. Try the demo data to see the timeline in action. +

+ +
+ ) : ( +
+ {/* Timeline Container - Expands with zoom */} +
+
+ +
+
+ + {/* Legend */} +
+
+
+ Event +
+
+
+ Person +
+
+
+ Place +
+
+ + {useDemoData && ( +
+ Showing demo data with events spanning 2024-2026 +
+ )} +
+ )} +
+ ) +} diff --git a/backends/advanced/webui/src/pages/TimelineRouter.tsx b/backends/advanced/webui/src/pages/TimelineRouter.tsx new file mode 100644 index 00000000..0e983ca6 --- /dev/null +++ b/backends/advanced/webui/src/pages/TimelineRouter.tsx @@ -0,0 +1,86 @@ +import { useState } from 'react' +import { Calendar } from 'lucide-react' +import FrappeGanttTimeline from './FrappeGanttTimeline' +import ReactGanttTimeline from './ReactGanttTimeline' +import MyceliaTimeline from './MyceliaTimeline' + +type TimelineImplementation = 'frappe' | 'react-gantt' | 'mycelia' + +export default function TimelineRouter() { + const [activeImplementation, setActiveImplementation] = useState('frappe') + + return ( +
+ {/* Header */} +
+
+

+ + Timeline +

+

+ Visualize your memories on an interactive timeline +

+
+
+ + {/* Tab Navigation */} +
+ +
+ + {/* Timeline Implementation */} +
+ {activeImplementation === 'frappe' && } + {activeImplementation === 'react-gantt' && } + {activeImplementation === 'mycelia' && } +
+
+ ) +} diff --git a/backends/advanced/webui/src/services/api.ts b/backends/advanced/webui/src/services/api.ts index d40508e8..2617cdaa 100644 --- a/backends/advanced/webui/src/services/api.ts +++ b/backends/advanced/webui/src/services/api.ts @@ -138,15 +138,19 @@ export const systemApi = { // Memory Configuration Management getMemoryConfigRaw: () => api.get('/api/admin/memory/config/raw'), - updateMemoryConfigRaw: (configYaml: string) => + updateMemoryConfigRaw: (configYaml: string) => api.post('/api/admin/memory/config/raw', configYaml, { headers: { 'Content-Type': 'text/plain' } }), - validateMemoryConfig: (configYaml: string) => + validateMemoryConfig: (configYaml: string) => api.post('/api/admin/memory/config/validate', configYaml, { headers: { 'Content-Type': 'text/plain' } }), reloadMemoryConfig: () => api.post('/api/admin/memory/config/reload'), + + // Memory Provider Management + getMemoryProvider: () => api.get('/api/admin/memory/provider'), + setMemoryProvider: (provider: string) => api.post('/api/admin/memory/provider', { provider }), } export const queueApi = { diff --git a/backends/advanced/webui/src/types/react-gantt-timeline.d.ts b/backends/advanced/webui/src/types/react-gantt-timeline.d.ts new file mode 100644 index 00000000..513337aa --- /dev/null +++ b/backends/advanced/webui/src/types/react-gantt-timeline.d.ts @@ -0,0 +1,45 @@ +declare module 'react-gantt-timeline' { + import { ComponentType } from 'react' + + export interface TimelineTask { + id: string + name: string + start: Date + end: Date + color?: string + } + + export interface TimelineConfig { + header?: { + top?: { + style?: React.CSSProperties + } + middle?: { + style?: React.CSSProperties + } + bottom?: { + style?: React.CSSProperties + } + } + taskList?: { + title?: string + label?: { + width?: string + } + columns?: Array<{ + id: number + title: string + fieldName: string + width: number + }> + } + } + + export interface TimelineProps { + data: TimelineTask[] + config?: TimelineConfig + } + + const Timeline: ComponentType + export default Timeline +} From 6ca997d3464e0e2bf4dd205e6eeab9ab101cdde3 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Fri, 5 Dec 2025 01:00:25 +0000 Subject: [PATCH 11/21] Merging of mycelia --- tests/endpoints/wedding_memory_test.robot | 92 +++++++++++++++++++ tests/infrastructure/infra_tests.robot | 2 +- .../websocket_streaming_tests.robot | 2 +- tests/resources/memory_keywords.robot | 39 ++++++-- tests/resources/queue_keywords.robot | 1 + tests/resources/transcript_verification.robot | 55 +---------- tests/setup/teardown_keywords.robot | 2 +- tests/setup/test_manager_keywords.robot | 79 +++++++++++++++- 8 files changed, 200 insertions(+), 72 deletions(-) create mode 100644 tests/endpoints/wedding_memory_test.robot diff --git a/tests/endpoints/wedding_memory_test.robot b/tests/endpoints/wedding_memory_test.robot new file mode 100644 index 00000000..6f731682 --- /dev/null +++ b/tests/endpoints/wedding_memory_test.robot @@ -0,0 +1,92 @@ +*** Settings *** +Documentation Test Creating Memory About Getting Married +... +... This test creates a memory about getting married in a week +... by sending a message to a chat session and extracting memories. + +Library RequestsLibrary +Library Collections +Resource ../setup/setup_keywords.robot +Resource ../setup/teardown_keywords.robot +Resource ../resources/memory_keywords.robot +Suite Setup Suite Setup +Suite Teardown Suite Teardown + + +*** Test Cases *** +Create Wedding Memory Via Add Memory Endpoint + [Documentation] Create a memory about getting married using POST /api/memories endpoint + [Tags] memory + + # Get initial memory count + ${initial_memories}= Get User Memories api + ${initial_count}= Get Length ${initial_memories} + Log Initial memory count: ${initial_count} + + # Create memory directly with wedding content + ${wedding_content}= Set Variable I'm getting married in one week! It's going to be at the botanical gardens with about 150 guests. We've been planning this for over a year and I'm so excited but also nervous. The ceremony is next Saturday at 4pm followed by a reception. + + # Prepare request body as dictionary + &{request_body}= Create Dictionary content=${wedding_content} source_id=wedding_test + + # Add memory via POST /api/memories + ${add_response}= POST On Session api /api/memories json=${request_body} + Should Be Equal As Integers ${add_response.status_code} 200 + ${add_data}= Set Variable ${add_response.json()} + Log Memory creation response: ${add_data} + + # Verify memory creation was successful + Should Be True ${add_data}[success] Memory creation should succeed + Should Be True ${add_data}[count] > 0 Should create at least one memory + Log Created ${add_data}[count] memory/memories with IDs: ${add_data}[memory_ids] + + # Wait a moment for memory to be fully stored + Sleep 2s + + # Get memories after extraction + ${final_memories}= Get User Memories api + ${final_count}= Get Length ${final_memories} + Log Final memory count: ${final_count} + + # Verify new memories were created + Should Be True ${final_count} > ${initial_count} New memories should be created + ${new_memory_count}= Evaluate ${final_count} - ${initial_count} + Log Created ${new_memory_count} new memory/memories + + # Search for the wedding memory + ${search_results}= Search Memories api wedding + Log Search results: ${search_results} + + # Verify search found the wedding memory + Should Be True len(${search_results}) > 0 Should find wedding-related memory + ${first_result}= Set Variable ${search_results}[0] + ${memory_content}= Convert To Lower Case ${first_result}[memory] + + # Verify memory contains wedding-related information + Should Contain Any ${memory_content} wedding married marry + ... Memory should contain wedding-related keywords + Log Found wedding memory: ${first_result}[memory] + + # Verify specific details are captured + ${should_contain_week}= Evaluate "week" in """${memory_content}""" + ${should_contain_botanical}= Evaluate "botanical" in """${memory_content}""" or "garden" in """${memory_content}""" + ${should_contain_saturday}= Evaluate "saturday" in """${memory_content}""" + + Log Memory contains 'week': ${should_contain_week} + Log Memory contains 'botanical/garden': ${should_contain_botanical} + Log Memory contains 'saturday': ${should_contain_saturday} + +Verify Memory Persists In Mycelia + [Documentation] Verify the wedding memory is stored in Mycelia and can be retrieved + [Tags] memory + + # Search for wedding memory + ${results}= Search Memories api getting married + Should Be True len(${results}) > 0 Wedding memory should be searchable + + ${wedding_memory}= Set Variable ${results}[0] + Should Not Be Empty ${wedding_memory}[id] Memory should have ID + Should Not Be Empty ${wedding_memory}[memory] Memory should have content + + Log Wedding memory ID: ${wedding_memory}[id] + Log Wedding memory content: ${wedding_memory}[memory] diff --git a/tests/infrastructure/infra_tests.robot b/tests/infrastructure/infra_tests.robot index d10c9476..93bf38ff 100644 --- a/tests/infrastructure/infra_tests.robot +++ b/tests/infrastructure/infra_tests.robot @@ -259,7 +259,7 @@ WebSocket Disconnect Conversation End Reason Test ${stream_id}= Open Audio Stream device_name=${device_name} # Send audio fast (no realtime pacing) to simulate disconnect before END signal - Send Audio Chunks To Stream ${stream_id} ${TEST_AUDIO_FILE} num_chunks=100 + Send Audio Chunks To Stream ${stream_id} ${TEST_AUDIO_FILE} num_chunks=200 # Wait for conversation job to be created and conversation_id to be populated ${conv_jobs}= Wait Until Keyword Succeeds 30s 2s diff --git a/tests/integration/websocket_streaming_tests.robot b/tests/integration/websocket_streaming_tests.robot index f2375261..eaf65047 100644 --- a/tests/integration/websocket_streaming_tests.robot +++ b/tests/integration/websocket_streaming_tests.robot @@ -175,7 +175,7 @@ Segment Timestamps Match Cropped Audio Log To Console Conversation 2 completed: ${conversation_id} # Wait for cropping job to complete - ${cropping_jobs}= Wait Until Keyword Succeeds 30s 2s + ${cropping_jobs}= Wait Until Keyword Succeeds 60s 2s ... Job Type Exists For Conversation process_cropping_job ${conversation_id} ${cropping_job}= Set Variable ${cropping_jobs}[0] Wait For Job Status ${cropping_job}[job_id] completed timeout=30s interval=2s diff --git a/tests/resources/memory_keywords.robot b/tests/resources/memory_keywords.robot index 4a02c40e..65e54629 100644 --- a/tests/resources/memory_keywords.robot +++ b/tests/resources/memory_keywords.robot @@ -21,6 +21,7 @@ Variables ../setup/test_env.py Get User Memories [Documentation] Get memories for authenticated user using session + ... Returns the list of memory objects directly. [Arguments] ${session} ${limit}=50 ${user_id}=${None} &{params}= Create Dictionary limit=${limit} @@ -30,59 +31,77 @@ Get User Memories END ${response}= GET On Session ${session} /api/memories params=${params} - RETURN ${response} + Should Be Equal As Integers ${response.status_code} 200 + ${data}= Set Variable ${response.json()} + ${memories}= Set Variable ${data}[memories] + RETURN ${memories} Get Memories With Transcripts [Documentation] Get memories with their source transcripts using session + ... Returns the list of memory objects with transcript data. [Arguments] ${session} ${limit}=50 &{params}= Create Dictionary limit=${limit} ${response}= GET On Session ${session} /api/memories/with-transcripts params=${params} - RETURN ${response} + Should Be Equal As Integers ${response.status_code} 200 + ${data}= Set Variable ${response.json()} + ${memories}= Set Variable ${data}[memories] + RETURN ${memories} Search Memories [Documentation] Search memories by query using session + ... Returns the list of search results directly. [Arguments] ${session} ${query} ${limit}=20 ${score_threshold}=0.0 &{params}= Create Dictionary query=${query} limit=${limit} score_threshold=${score_threshold} ${response}= GET On Session ${session} /api/memories/search params=${params} - RETURN ${response} + Should Be Equal As Integers ${response.status_code} 200 + ${data}= Set Variable ${response.json()} + ${results}= Set Variable ${data}[results] + RETURN ${results} Delete Memory [Documentation] Delete a specific memory using session + ... Returns True if deletion was successful, False otherwise. [Arguments] ${session} ${memory_id} ${response}= DELETE On Session ${session} /api/memories/${memory_id} - RETURN ${response} + ${success}= Evaluate ${response.status_code} == 200 + RETURN ${success} Get Unfiltered Memories [Documentation] Get all memories including fallback transcript memories using session + ... Returns the list of unfiltered memory objects directly. [Arguments] ${session} ${limit}=50 &{params}= Create Dictionary limit=${limit} ${response}= GET On Session ${session} /api/memories/unfiltered params=${params} - RETURN ${response} + Should Be Equal As Integers ${response.status_code} 200 + ${data}= Set Variable ${response.json()} + ${memories}= Set Variable ${data}[memories] + RETURN ${memories} Get All Memories Admin [Documentation] Get all memories across all users (admin only) using session + ... Returns the list of all memory objects across all users. [Arguments] ${session} ${limit}=200 &{params}= Create Dictionary limit=${limit} ${response}= GET On Session ${session} /api/memories/admin params=${params} - RETURN ${response} + Should Be Equal As Integers ${response.status_code} 200 + ${data}= Set Variable ${response.json()} + ${memories}= Set Variable ${data}[memories] + RETURN ${memories} Count User Memories [Documentation] Count memories for a user using session [Arguments] ${session} - ${response}= Get User Memories ${session} 1000 - Should Be Equal As Integers ${response.status_code} 200 - ${memories_data}= Set Variable ${response.json()} - ${memories}= Set Variable ${memories_data}[memories] + ${memories}= Get User Memories ${session} 1000 ${count}= Get Length ${memories} RETURN ${count} diff --git a/tests/resources/queue_keywords.robot b/tests/resources/queue_keywords.robot index 32f8b7fa..d481f3dd 100644 --- a/tests/resources/queue_keywords.robot +++ b/tests/resources/queue_keywords.robot @@ -4,6 +4,7 @@ Library RequestsLibrary Library Collections Variables ../setup/test_env.py Resource session_keywords.robot +Resource ../setup/setup_keywords.robot *** Keywords *** diff --git a/tests/resources/transcript_verification.robot b/tests/resources/transcript_verification.robot index 068f63f4..2d9e7a45 100644 --- a/tests/resources/transcript_verification.robot +++ b/tests/resources/transcript_verification.robot @@ -254,33 +254,6 @@ Verify Segments Match Expected Timestamps Log All ${actual_count} segments matched expected timestamps within ${tolerance}s tolerance INFO - Verify Transcript Content - [Documentation] Verify transcript contains expected content and quality - [Arguments] ${conversation} ${expected_keywords} ${min_length}=50 - - Dictionary Should Contain Key ${conversation} transcript - ${transcript}= Set Variable ${conversation}[transcript] - Should Not Be Empty ${transcript} - - # Check length - ${transcript_length}= Get Length ${transcript} - Should Be True ${transcript_length} >= ${min_length} Transcript too short: ${transcript_length} - - # Check for expected keywords - ${transcript_lower}= Convert To Lower Case ${transcript} - FOR ${keyword} IN @{expected_keywords} - ${keyword_lower}= Convert To Lower Case ${keyword} - Should Contain ${transcript_lower} ${keyword_lower} Missing keyword: ${keyword} - END - - # Verify segments exist - Dictionary Should Contain Key ${conversation} segments - ${segments}= Set Variable ${conversation}[segments] - ${segment_count}= Get Length ${segments} - Should Be True ${segment_count} > 0 No segments found - - Log Transcript verification passed: ${transcript_length} chars, ${segment_count} segments INFO - Verify Transcript Content [Documentation] Verify transcript contains expected content and quality [Arguments] ${conversation} ${expected_keywords} ${min_length}=50 @@ -307,30 +280,4 @@ Verify Transcript Content Should Be True ${segment_count} > 0 No segments found Log Transcript verification passed: ${transcript_length} chars, ${segment_count} segments INFO - - Verify Transcript Content - [Documentation] Verify transcript contains expected content and quality - [Arguments] ${conversation} ${expected_keywords} ${min_length}=50 - - Dictionary Should Contain Key ${conversation} transcript - ${transcript}= Set Variable ${conversation}[transcript] - Should Not Be Empty ${transcript} - - # Check length - ${transcript_length}= Get Length ${transcript} - Should Be True ${transcript_length} >= ${min_length} Transcript too short: ${transcript_length} - - # Check for expected keywords - ${transcript_lower}= Convert To Lower Case ${transcript} - FOR ${keyword} IN @{expected_keywords} - ${keyword_lower}= Convert To Lower Case ${keyword} - Should Contain ${transcript_lower} ${keyword_lower} Missing keyword: ${keyword} - END - - # Verify segments exist - Dictionary Should Contain Key ${conversation} segments - ${segments}= Set Variable ${conversation}[segments] - ${segment_count}= Get Length ${segments} - Should Be True ${segment_count} > 0 No segments found - - Log Transcript verification passed: ${transcript_length} chars, ${segment_count} segments INFO + \ No newline at end of file diff --git a/tests/setup/teardown_keywords.robot b/tests/setup/teardown_keywords.robot index 4553ad0a..3be0eb6d 100644 --- a/tests/setup/teardown_keywords.robot +++ b/tests/setup/teardown_keywords.robot @@ -38,7 +38,7 @@ Dev Mode Teardown Log To Console \n=== Dev Mode Teardown (Default) === Log To Console โœ“ Keeping containers running for next test run Log To Console Tip: Use 'TEST_MODE=prod' for full cleanup or run manually: - Log To Console docker compose -f backends/advanced/docker-compose-ci.yml down -v + Log To Console docker compose -f backends/advanced/docker-compose-test.yml down -v # Only delete HTTP sessions Delete All Sessions diff --git a/tests/setup/test_manager_keywords.robot b/tests/setup/test_manager_keywords.robot index a7ad5783..60ffc107 100644 --- a/tests/setup/test_manager_keywords.robot +++ b/tests/setup/test_manager_keywords.robot @@ -19,11 +19,13 @@ Library Process Library String Library Collections Library DateTime +Library RequestsLibrary Variables test_env.py Resource ../resources/audio_keywords.robot Resource ../resources/conversation_keywords.robot Resource ../resources/websocket_keywords.robot Resource ../resources/queue_keywords.robot +Resource ../resources/session_keywords.robot *** Keywords *** @@ -53,11 +55,8 @@ Clear Test Databases # Clear admin user's registered_clients dict to prevent client_id counter increments Run Process docker exec advanced-mongo-test-1 mongosh test_db --eval "db.users.updateOne({'email':'${ADMIN_EMAIL}'}, {\\$set: {'registered_clients': {}}})" shell=True - # Clear Qdrant collections - # Note: Fixture memories will be lost here unless we implement Qdrant metadata filtering - Run Process curl -s -X DELETE http://localhost:6337/collections/memories shell=True - Run Process curl -s -X DELETE http://localhost:6337/collections/conversations shell=True - Log To Console Qdrant collections cleared + # Clear memory provider data (Qdrant or Mycelia) + Clear Memory Provider Data # Clear audio files (except fixtures subfolder) Run Process bash -c find ${EXECDIR}/backends/advanced/data/test_audio_chunks -maxdepth 1 -name "*.wav" -delete || true shell=True @@ -172,3 +171,73 @@ Test Cleanup # Try to cleanup audio streams if the keyword exists (websocket tests) Run Keyword And Ignore Error Cleanup All Audio Streams Flush In Progress Jobs + +Clear Memory Provider Data + [Documentation] Clear memory data based on the current memory provider + ... Automatically detects provider and clears appropriately: + ... - friend_lite: Clears Qdrant collections + ... - mycelia: Deletes all memories via Mycelia API + + # Check current provider from health endpoint + TRY + Create Session provider_check_session ${API_URL} verify=False + ${response}= GET On Session provider_check_session /health + ${health_data}= Set Variable ${response.json()} + ${memory_service}= Get From Dictionary ${health_data}[services] memory_service + ${provider}= Set Variable ${memory_service}[provider] + Delete All Sessions + + IF '${provider}' == 'mycelia' + Log To Console Clearing Mycelia memories... + Clear Mycelia Memories + ELSE IF '${provider}' == 'friend_lite' + Log To Console Clearing Qdrant collections... + Clear Qdrant Collections + ELSE + Log To Console Unknown provider '${provider}', attempting to clear both Qdrant and Mycelia... + Clear Qdrant Collections + Run Keyword And Ignore Error Clear Mycelia Memories + END + EXCEPT + Log To Console Could not determine memory provider, clearing Qdrant as fallback + Clear Qdrant Collections + END + +Clear Qdrant Collections + [Documentation] Clear Qdrant vector collections + Run Process curl -s -X DELETE http://localhost:6337/collections/memories shell=True + Run Process curl -s -X DELETE http://localhost:6337/collections/conversations shell=True + Log To Console โœ“ Qdrant collections cleared + +Clear Mycelia Memories + [Documentation] Clear all Mycelia memories for the admin user via backend API + ... Uses the backend's delete endpoint which calls Mycelia's delete_all_user_memories + + # Create admin session for API calls + Create API Session mycelia_cleanup_session + + # Get admin user ID + ${response}= GET On Session mycelia_cleanup_session /api/users/me + Should Be Equal As Integers ${response.status_code} 200 + ${user_data}= Set Variable ${response.json()} + ${user_id}= Set Variable ${user_data}[id] + + # Get all memories to delete + ${memories_response}= GET On Session mycelia_cleanup_session /api/memories?limit=1000 + Should Be Equal As Integers ${memories_response.status_code} 200 + ${memories_data}= Set Variable ${memories_response.json()} + ${memories}= Set Variable ${memories_data}[memories] + ${memory_count}= Get Length ${memories} + + # Delete each memory + ${deleted_count}= Set Variable 0 + FOR ${memory} IN @{memories} + ${memory_id}= Set Variable ${memory}[id] + ${delete_response}= DELETE On Session mycelia_cleanup_session /api/memories/${memory_id} expected_status=any + IF ${delete_response.status_code} == 200 + ${deleted_count}= Evaluate ${deleted_count} + 1 + END + END + + Delete All Sessions + Log To Console โœ“ Cleared ${deleted_count} Mycelia memories for admin user From b767760ecd73ab1d9c6f5882189c08e16c9506b9 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Fri, 5 Dec 2025 13:57:21 +0000 Subject: [PATCH 12/21] Quick commit --- .env.secrets.template | 59 + .gitignore | 4 + ACT_GUIDE.md | 82 - CLAUDE.md | 24 + DOCKER-COMPOSE.md | 462 + ENVIRONMENTS.md | 445 + Makefile | 525 +- SETUP.md | 218 + WIZARD.md | 483 + backends/advanced/compose/backend.yml | 74 + backends/advanced/compose/frontend.yml | 24 + backends/advanced/compose/infrastructure.yml | 57 + backends/advanced/compose/mycelia.yml | 9 + .../advanced/compose/optional-services.yml | 116 + backends/advanced/compose/overrides/dev.yml | 5 + backends/advanced/compose/overrides/prod.yml | 82 + backends/advanced/compose/overrides/test.yml | 143 + backends/advanced/docker-compose.override.yml | 22 + backends/advanced/docker-compose.yml | 272 +- backends/advanced/generate-caddyfile.sh | 18 + backends/advanced/webui/Dockerfile | 6 +- backends/advanced/webui/Dockerfile.dev | 17 +- backends/advanced/webui/index.html | 1 + backends/advanced/webui/package-lock.json | 12590 ++++++++++++---- backends/advanced/webui/package.json | 13 +- backends/advanced/webui/vite.config.ts | 7 + compose/advanced-backend.yml | 7 + compose/asr-services.yml | 28 + compose/mycelia.yml | 69 + compose/observability.yml | 50 + compose/openmemory.yml | 50 + compose/speaker-recognition.yml | 34 + docker-compose.yml | 39 + extras/openmemory-mcp/docker-compose.yml | 29 +- extras/speaker-recognition/run-test.sh | 4 +- scripts/lib/env_utils.py | 19 +- start-env.sh | 260 + tests/setup/test_env.py | 48 +- 38 files changed, 13349 insertions(+), 3046 deletions(-) create mode 100644 .env.secrets.template delete mode 100644 ACT_GUIDE.md create mode 100644 DOCKER-COMPOSE.md create mode 100644 ENVIRONMENTS.md create mode 100644 SETUP.md create mode 100644 WIZARD.md create mode 100644 backends/advanced/compose/backend.yml create mode 100644 backends/advanced/compose/frontend.yml create mode 100644 backends/advanced/compose/infrastructure.yml create mode 100644 backends/advanced/compose/mycelia.yml create mode 100644 backends/advanced/compose/optional-services.yml create mode 100644 backends/advanced/compose/overrides/dev.yml create mode 100644 backends/advanced/compose/overrides/prod.yml create mode 100644 backends/advanced/compose/overrides/test.yml create mode 100644 backends/advanced/docker-compose.override.yml create mode 100644 backends/advanced/generate-caddyfile.sh create mode 100644 compose/advanced-backend.yml create mode 100644 compose/asr-services.yml create mode 100644 compose/mycelia.yml create mode 100644 compose/observability.yml create mode 100644 compose/openmemory.yml create mode 100644 compose/speaker-recognition.yml create mode 100644 docker-compose.yml create mode 100755 start-env.sh diff --git a/.env.secrets.template b/.env.secrets.template new file mode 100644 index 00000000..95006dc0 --- /dev/null +++ b/.env.secrets.template @@ -0,0 +1,59 @@ +# ======================================== +# Friend-Lite Secrets Template +# ======================================== +# Copy this file to .env.secrets and fill in your actual values +# .env.secrets is gitignored and should NEVER be committed +# +# Usage: cp .env.secrets.template .env.secrets + +# ======================================== +# AUTHENTICATION & SECURITY +# ======================================== + +# JWT secret key - make this random and long +AUTH_SECRET_KEY=your-super-secret-jwt-key-change-this-to-something-random + +# Admin account credentials +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=change-this-secure-password + +# ======================================== +# LLM API KEYS +# ======================================== + +# OpenAI API key +OPENAI_API_KEY=sk-your-openai-key-here + +# Mistral API key (optional - only if using Mistral transcription) +MISTRAL_API_KEY= + +# Groq API key (optional - only if using Groq as LLM provider) +GROQ_API_KEY= + +# ======================================== +# SPEECH-TO-TEXT API KEYS +# ======================================== + +# Deepgram API key +DEEPGRAM_API_KEY=your-deepgram-key-here + +# ======================================== +# SPEAKER RECOGNITION +# ======================================== + +# Hugging Face token for speaker recognition models +HF_TOKEN=hf_your_huggingface_token_here + +# ======================================== +# EXTERNAL SERVICES (OPTIONAL) +# ======================================== + +# Langfuse telemetry (optional) +LANGFUSE_PUBLIC_KEY= +LANGFUSE_SECRET_KEY= + +# Ngrok authtoken (optional - for external access) +NGROK_AUTHTOKEN= + +# Neo4j credentials (optional) +NEO4J_PASSWORD=your-neo4j-password diff --git a/.gitignore b/.gitignore index b2b052b3..45bd86ba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ *.wav **/*.env !**/.env.template +.env.secrets +.env.backup.* +config.env.backup.* **/memory_config.yaml !**/memory_config.yaml.template example/* @@ -82,3 +85,4 @@ log.html output.xml report.html .secrets +extras/openmemory-mcp/.env.openmemory diff --git a/ACT_GUIDE.md b/ACT_GUIDE.md deleted file mode 100644 index fcb3ae31..00000000 --- a/ACT_GUIDE.md +++ /dev/null @@ -1,82 +0,0 @@ -# Testing GitHub Actions Locally with Act - -## Setup Complete โœ“ - -Act is installed and configured. Your `.secrets` file is ready (gitignored). - -## Quick Start - -### 1. Dry Run (See what would execute) -```bash -act pull_request -W .github/workflows/robot-tests.yml -n --container-architecture linux/amd64 -``` - -### 2. Run Robot Tests Locally (Full GitHub Actions simulation) -```bash -act pull_request -W .github/workflows/robot-tests.yml \ - --secret-file .secrets \ - --container-architecture linux/amd64 -``` - -### 3. Run with Verbose Output -```bash -act pull_request -W .github/workflows/robot-tests.yml \ - --secret-file .secrets \ - --container-architecture linux/amd64 \ - -v -``` - -### 4. Skip Image Pull (After first run) -```bash -act pull_request -W .github/workflows/robot-tests.yml \ - --secret-file .secrets \ - --container-architecture linux/amd64 \ - --pull=false -``` - -## Important Notes - -- **First run downloads ~20GB Docker image** - be patient -- **M-series Mac**: Always use `--container-architecture linux/amd64` -- **Secrets file**: `.secrets` contains your API keys (gitignored) -- **Resource intensive**: Docker-in-Docker uses significant CPU/RAM -- **Not 100% identical**: Some GitHub-specific features may behave differently - -## Editing Secrets - -```bash -nano .secrets -``` - -Format: -``` -DEEPGRAM_API_KEY=your-key-here -OPENAI_API_KEY=your-key-here -``` - -## Troubleshooting - -### Out of disk space -```bash -# Clean up act containers -docker system prune -a -``` - -### Workflow fails differently than GitHub -- Act uses different runner images -- Some GitHub Actions may not be fully compatible -- Check act logs vs GitHub Actions logs - -### Kill running act job -```bash -# Ctrl+C or: -docker ps | grep act | awk '{print $1}' | xargs docker kill -``` - -## Why Use Act? - -- Test workflows without pushing to GitHub -- Faster iteration during workflow development -- Debug CI-specific issues locally -- Save GitHub Actions minutes - diff --git a/CLAUDE.md b/CLAUDE.md index 0f579d33..8c7af37a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -209,6 +209,29 @@ Optional: - **API Security**: JWT tokens required for all endpoints and WebSocket connections - **Admin Bootstrap**: Automatic admin account creation with ADMIN_EMAIL/ADMIN_PASSWORD +## Quick Setup + +Friend-Lite includes an interactive setup wizard that guides you through complete configuration: + +```bash +make wizard +``` + +This single command will: +1. ๐Ÿ” Configure secrets (API keys, passwords) +2. ๐ŸŒ Optionally setup Tailscale for distributed deployment +3. ๐Ÿ“ฆ Create a custom environment +4. ๐Ÿš€ Provide clear instructions to start services + +**Individual setup steps:** +```bash +make setup-secrets # Configure API keys and passwords +make setup-tailscale # Configure Tailscale and SSL (optional) +make setup-environment # Create environment config +``` + +For complete wizard documentation, see [`WIZARD.md`](WIZARD.md). + ## Configuration ### Required Environment Variables @@ -396,6 +419,7 @@ Project includes `.cursor/rules/always-plan-first.mdc` requiring understanding b For detailed technical documentation, see: - **[@docs/wyoming-protocol.md](docs/wyoming-protocol.md)**: WebSocket communication protocol details - **[@docs/memory-providers.md](docs/memory-providers.md)**: In-depth memory provider comparison and setup +- **[@backends/advanced/Docs/mycelia-setup.md](backends/advanced/Docs/mycelia-setup.md)**: Complete Mycelia setup guide with auto-login and OAuth - **[@docs/versioned-processing.md](docs/versioned-processing.md)**: Transcript and memory versioning details - **[@docs/api-reference.md](docs/api-reference.md)**: Complete endpoint documentation with examples - **[@docs/speaker-recognition.md](docs/speaker-recognition.md)**: Advanced analysis and live inference features diff --git a/DOCKER-COMPOSE.md b/DOCKER-COMPOSE.md new file mode 100644 index 00000000..14e4bc8d --- /dev/null +++ b/DOCKER-COMPOSE.md @@ -0,0 +1,462 @@ +# Friend-Lite Docker Compose Guide + +Friend-Lite uses a **unified root-level Docker Compose** structure that makes it easy to start all services from one place. + +## Quick Start + +```bash +# From project root +cd /path/to/friend-lite + +# Create shared network (first time only) +docker network create chronicle-network + +# Start core services +docker compose up + +# Start with optional services +docker compose --profile mycelia up # With Mycelia memory service +docker compose --profile speaker up # With speaker recognition +docker compose --profile asr up # With offline ASR (Parakeet) +docker compose --profile observability up # With Langfuse monitoring + +# Multiple profiles +docker compose --profile mycelia --profile speaker up +``` + +## Project Structure + +``` +friend-lite/ # PROJECT ROOT +โ”œโ”€โ”€ docker-compose.yml # Root compose file (YOU START HERE) +โ”œโ”€โ”€ compose/ +โ”‚ โ”œโ”€โ”€ advanced-backend.yml # Includes backends/advanced/ +โ”‚ โ”œโ”€โ”€ asr-services.yml # Offline ASR (Parakeet) +โ”‚ โ”œโ”€โ”€ speaker-recognition.yml # Voice identification +โ”‚ โ”œโ”€โ”€ openmemory.yml # OpenMemory MCP server +โ”‚ โ””โ”€โ”€ observability.yml # Langfuse monitoring +โ”œโ”€โ”€ backends/ +โ”‚ โ””โ”€โ”€ advanced/ +โ”‚ โ”œโ”€โ”€ docker-compose.yml # Advanced backend (included by root) +โ”‚ โ”œโ”€โ”€ compose/ +โ”‚ โ”‚ โ”œโ”€โ”€ infrastructure.yml # Mongo, Redis, Qdrant +โ”‚ โ”‚ โ”œโ”€โ”€ backend.yml # Friend-backend, Workers +โ”‚ โ”‚ โ”œโ”€โ”€ frontend.yml # WebUI +โ”‚ โ”‚ โ”œโ”€โ”€ mycelia.yml # Mycelia (--profile mycelia) +โ”‚ โ”‚ โ”œโ”€โ”€ optional-services.yml # Caddy, Ollama, etc. +โ”‚ โ”‚ โ””โ”€โ”€ overrides/ +โ”‚ โ”‚ โ”œโ”€โ”€ dev.yml # Development settings +โ”‚ โ”‚ โ”œโ”€โ”€ test.yml # Test environment +โ”‚ โ”‚ โ””โ”€โ”€ prod.yml # Production config +โ”‚ โ””โ”€โ”€ .env # Backend configuration +โ””โ”€โ”€ extras/ + โ”œโ”€โ”€ asr-services/ # ASR services + โ”œโ”€โ”€ speaker-recognition/ # Speaker identification + โ”œโ”€โ”€ mycelia/ # Memory service + โ””โ”€โ”€ openmemory-mcp/ # OpenMemory server +``` + +## Service Profiles + +| Profile | Services | When to Use | +|---------|----------|-------------| +| **(none)** | Core backend, WebUI, databases | Default development | +| `mycelia` | + Mycelia memory service | Advanced memory features | +| `speaker` | + Speaker recognition | Voice identification | +| `asr` | + Parakeet offline ASR | Offline transcription | +| `openmemory` | + OpenMemory MCP server | Cross-client memory | +| `observability` | + Langfuse monitoring | LLM tracing/debugging | +| `https` | + Caddy reverse proxy | HTTPS for microphone access | + +## Usage Examples + +### Development (Default) + +```bash +# Start from project root +docker compose up + +# Services started: +# - mongo (27017) +# - redis (6379) +# - qdrant (6033/6034) +# - friend-backend (8000) +# - workers +# - webui (3010) +``` + +### With Mycelia + +```bash +docker compose --profile mycelia up + +# Additional services: +# - mycelia-backend (5100) +# - mycelia-frontend (3003) +``` + +### With Speaker Recognition + +```bash +docker compose --profile speaker up + +# Additional services: +# - speaker-recognition (8085) +``` + +### With Offline ASR + +```bash +docker compose --profile asr up + +# Additional services: +# - parakeet-asr (8767) +``` + +### Everything + +```bash +docker compose --profile mycelia --profile speaker --profile asr up + +# Starts all available services +``` + +### Testing Environment + +```bash +# Uses isolated test databases and ports +docker compose -f docker-compose.yml -f backends/advanced/compose/overrides/test.yml up + +# Test services use different ports: +# - Backend: 8001 (dev: 8000) +# - WebUI: 3001 (dev: 3010) +# - Mongo: 27018 (dev: 27017) +# - Redis: 6380 (dev: 6379) +``` + +### Production + +```bash +# From project root +docker compose -f docker-compose.yml -f backends/advanced/compose/overrides/prod.yml up -d + +# Production changes: +# - No source code mounting +# - Resource limits applied +# - Always restart policy +``` + +## Environment Configuration + +### backends/advanced/.env + +This is the main configuration file for the backend: + +```bash +# Authentication +AUTH_SECRET_KEY=your-super-secret-jwt-key +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=your-secure-password + +# LLM Configuration +LLM_PROVIDER=openai +OPENAI_API_KEY=your-openai-key +OPENAI_MODEL=gpt-4o-mini + +# Speech-to-Text +DEEPGRAM_API_KEY=your-deepgram-key +TRANSCRIPTION_PROVIDER=deepgram + +# Memory Provider +MEMORY_PROVIDER=friend_lite # or: mycelia, openmemory_mcp + +# Optional: Speaker Recognition +SPEAKER_SERVICE_URL=http://speaker-recognition:8085 + +# Optional: Offline ASR +PARAKEET_ASR_URL=http://parakeet-asr:8767 +``` + +### Port Customization + +Override default ports via environment variables: + +```bash +# In your shell or .env file +export BACKEND_PORT=9000 +export WEBUI_PORT=3015 +export MONGO_PORT=27018 + +docker compose up +``` + +## Common Commands + +```bash +# View merged configuration +docker compose config + +# List all services +docker compose config --services + +# List services with profiles +docker compose --profile mycelia config --services + +# Start specific services only +docker compose up mongo redis qdrant + +# View logs +docker compose logs -f friend-backend +docker compose logs -f # All services + +# Stop everything +docker compose down + +# Stop and remove volumes (โš ๏ธ DELETES DATA!) +docker compose down -v + +# Rebuild and restart +docker compose build +docker compose up --build + +# Restart single service +docker compose restart friend-backend +``` + +## Service Access + +| Service | URL | Default Port | +|---------|-----|--------------| +| Backend API | http://localhost:8000 | 8000 | +| Backend Health | http://localhost:8000/readiness | 8000 | +| Web UI | http://localhost:3010 | 3010 | +| Mycelia Backend | http://localhost:5100 | 5100 | +| Mycelia Frontend | http://localhost:3003 | 3003 | +| Speaker Recognition | http://localhost:8085 | 8085 | +| Parakeet ASR | http://localhost:8767 | 8767 | +| OpenMemory API | http://localhost:8765 | 8765 | +| Langfuse | http://localhost:3000 | 3000 | +| MongoDB | mongodb://localhost:27017 | 27017 | +| Redis | redis://localhost:6379 | 6379 | +| Qdrant HTTP | http://localhost:6034 | 6034 | +| Qdrant gRPC | http://localhost:6033 | 6033 | + +## Architecture Benefits + +### Before: Multiple Compose Files + +```bash +# Had to remember which directory to cd into +cd backends/advanced && docker compose up +cd extras/speaker-recognition && docker compose up +cd extras/asr-services && docker compose up +# ... manage multiple compose instances separately +``` + +### After: Unified Root Compose + +```bash +# Start everything from project root +docker compose --profile speaker --profile asr up + +# Services coordinate automatically +# Shared network, unified management +``` + +### Why This Is Better + +1. **Single Entry Point**: Always start from project root +2. **Unified Control**: One command starts all related services +3. **Modular**: Include only what you need via profiles +4. **Environment Switching**: Easy dev/test/prod switching +5. **Clean Configs**: No redundant environment variables + +## Environment Switching + +### Development (Default) + +```bash +# From project root +docker compose up + +# Uses: +# - backends/advanced/docker-compose.yml (includes dev.yml) +# - Source code mounted for hot reload +# - Development-friendly settings +``` + +### Testing + +```bash +# Isolated test environment +docker compose -f docker-compose.yml \ + -f backends/advanced/compose/overrides/test.yml up + +# Uses: +# - Different ports (no conflicts) +# - Test database (test_db) +# - Test credentials +# - Fast timeouts for testing +``` + +### Production + +```bash +# Production configuration +docker compose -f docker-compose.yml \ + -f backends/advanced/compose/overrides/prod.yml up -d + +# Uses: +# - No source mounting +# - Resource limits +# - Always restart policy +# - Production-ready settings +``` + +## Advanced: Selective Service Management + +### Start Only Infrastructure + +```bash +docker compose up mongo redis qdrant -d +``` + +### Start Only Backend (assumes infra running) + +```bash +docker compose up friend-backend workers +``` + +### Add Services Incrementally + +```bash +# Start core +docker compose up -d + +# Later, add speaker recognition +docker compose --profile speaker up -d + +# Even later, add mycelia +docker compose --profile mycelia up -d +``` + +## Troubleshooting + +### "Network chronicle-network not found" + +```bash +# Create the shared network +docker network create chronicle-network + +# Then retry +docker compose up +``` + +### Port Conflicts + +```bash +# Check what's using ports +lsof -i :8000 +lsof -i :27017 + +# Stop conflicting services +docker compose down + +# Or use custom ports +BACKEND_PORT=9000 docker compose up +``` + +### "Service conflicts with imported resource" + +This means a service is defined in multiple compose files. Check: +- Are you accidentally including the same compose file twice? +- Do you have duplicate service names? + +### Services Not Starting with Profile + +Ensure you use `--profile`: + +```bash +# โŒ Wrong - mycelia won't start +docker compose up + +# โœ… Correct - mycelia starts +docker compose --profile mycelia up +``` + +### View Merged Configuration + +```bash +# See final merged config +docker compose config + +# With profiles +docker compose --profile mycelia --profile speaker config + +# Save to file +docker compose config > merged-config.yml +``` + +## Migration from Old Structure + +If you were previously running services from individual directories: + +### Old Way + +```bash +# Multiple terminals, multiple directories +cd backends/advanced && docker compose up +cd extras/speaker-recognition && docker compose up +``` + +### New Way + +```bash +# Single command from project root +cd /path/to/friend-lite +docker compose --profile speaker up +``` + +## Next Steps + +1. **First Time Setup**: + ```bash + cd /path/to/friend-lite + docker network create chronicle-network + cp backends/advanced/.env.template backends/advanced/.env + # Edit .env with your API keys + ``` + +2. **Start Development**: + ```bash + docker compose up + ``` + +3. **Access Services**: + - Backend: http://localhost:8000 + - Web UI: http://localhost:3010 + - Backend Health: http://localhost:8000/readiness + +4. **Enable Optional Services**: + ```bash + # Add services as needed + docker compose --profile mycelia up + docker compose --profile speaker up + ``` + +## Additional Resources + +- [Backend Compose Guide](backends/advanced/DOCKER-COMPOSE-GUIDE.md) - Detailed backend-specific docs +- [Docker Compose Include Documentation](https://docs.docker.com/compose/how-tos/multiple-compose-files/include/) +- [Docker Compose Profiles](https://docs.docker.com/compose/how-tos/profiles/) + +## Summary + +**Root Docker Compose** gives you: +- โœ… Single entry point for all services +- โœ… Unified service management +- โœ… Profile-based optional services +- โœ… Easy environment switching +- โœ… Clean, modular configuration + +Always start from **project root** (`/path/to/friend-lite/`) and use `docker compose` commands! diff --git a/ENVIRONMENTS.md b/ENVIRONMENTS.md new file mode 100644 index 00000000..469c5836 --- /dev/null +++ b/ENVIRONMENTS.md @@ -0,0 +1,445 @@ +# Multi-Environment Management + +Friend-Lite supports running multiple environments simultaneously with isolated databases and different ports. This is perfect for: +- **Git worktrees** - Work on multiple branches simultaneously +- **Feature development** - Isolated testing environments +- **Parallel testing** - Run tests while developing + +## Prerequisites + +### 1. Set Up Secrets (First Time Only) + +Before starting any environment, create your secrets file: + +```bash +# Copy the template +cp .env.secrets.template .env.secrets + +# Edit with your actual credentials +nano .env.secrets +``` + +Required secrets: +- `AUTH_SECRET_KEY` - JWT secret for authentication +- `ADMIN_EMAIL` / `ADMIN_PASSWORD` - Admin account credentials +- `OPENAI_API_KEY` - For LLM memory extraction +- `DEEPGRAM_API_KEY` - For transcription +- `HF_TOKEN` - For speaker recognition (if using) + +**โš ๏ธ Important**: `.env.secrets` is gitignored and will never be committed. Each developer needs their own copy. + +## Quick Start + +### Option 1: Using start-env.sh (Recommended) + +```bash +# Start development environment +./start-env.sh dev + +# Start feature branch environment +./start-env.sh feature-123 + +# Start with additional profiles +./start-env.sh dev --profile mycelia +./start-env.sh feature-123 --profile mycelia --profile speaker +``` + +### Option 2: Using Makefile + +```bash +# List available environments +make env-list + +# Start environment +make env-start ENV=dev +make env-start ENV=feature-123 OPTS="--profile mycelia" + +# Stop environment +make env-stop ENV=dev + +# Clean environment data +make env-clean ENV=dev + +# Show status of all environments +make env-status +``` + +## How It Works + +### 1. Shared Infrastructure + +All environments share the same infrastructure services (defined at root level): +- MongoDB (single instance, multiple databases) +- Redis (single instance) +- Qdrant (single instance) + +### 2. Environment-Specific + +Each environment gets: +- **Different ports** (via PORT_OFFSET) +- **Different database names** (e.g., `friend-lite-dev`, `friend-lite-feature-123`) +- **Isolated data directories** (e.g., `data-dev/`, `data-feature-123/`) +- **Separate containers** (via COMPOSE_PROJECT_NAME) + +### 3. Configuration Structure + +``` +environments/ +โ”œโ”€โ”€ dev.env # Development environment +โ”œโ”€โ”€ test.env # Test environment +โ”œโ”€โ”€ feature-123.env # Feature branch +โ””โ”€โ”€ your-branch.env # Create your own! +``` + +## Creating a New Environment + +Create a new file in `environments/`: + +```bash +# environments/my-feature.env +# My Feature Environment + +ENV_NAME=my-feature + +# Port offset (1000 = ports 9000, 4010, 28017, etc.) +PORT_OFFSET=1000 + +# Database names (must be unique per environment) +MONGODB_DATABASE=friend-lite-my-feature +MYCELIA_DB=mycelia-my-feature + +# Data directory (must be unique per environment) +DATA_DIR=backends/advanced/data-my-feature + +# Container prefix (must be unique per environment) +COMPOSE_PROJECT_NAME=friend-lite-my-feature + +# Services to enable +SERVICES=backend,webui,mycelia +``` + +Then start it: + +```bash +./start-env.sh my-feature +``` + +## Port Allocation + +Environments automatically calculate ports based on `PORT_OFFSET`: + +| Service | Base Port | Offset 0 (dev) | Offset 1000 (feature) | Offset 2000 | +|---------|-----------|----------------|-----------------------|-------------| +| Backend | 8000 | 8000 | 9000 | 10000 | +| WebUI | 3010 | 3010 | 4010 | 5010 | +| MongoDB | 27017 | 27017 | 28017 | 29017 | +| Redis | 6379 | 6379 | 7379 | 8379 | +| Qdrant HTTP | 6034 | 6034 | 7034 | 8034 | +| Mycelia Backend | 5100 | 5100 | 6100 | 7100 | + +**Pro tip:** Use PORT_OFFSET in multiples of 1000 to avoid conflicts. + +## Database Isolation + +Each environment has its own database: + +``` +MongoDB Instance (shared) +โ”œโ”€โ”€ friend-lite-dev # Dev environment +โ”œโ”€โ”€ friend-lite-test # Test environment +โ”œโ”€โ”€ friend-lite-feature-123 # Feature branch +โ”œโ”€โ”€ mycelia-dev # Dev mycelia +โ”œโ”€โ”€ mycelia-test # Test mycelia +โ””โ”€โ”€ mycelia-feature-123 # Feature mycelia +``` + +**Why this works:** +- โœ… Shared MongoDB instance (efficient) +- โœ… Isolated databases per environment (no conflicts) +- โœ… Easy cleanup (drop database when done) + +## Example Workflows + +### Working on Multiple Feature Branches + +```bash +# Terminal 1: Main development +cd ~/projects/friend-lite +./start-env.sh dev + +# Terminal 2: Feature branch (git worktree) +cd ~/projects/friend-lite-feature-auth +./start-env.sh feature-auth + +# Terminal 3: Another feature +cd ~/projects/friend-lite-feature-ui +./start-env.sh feature-ui + +# Now you have 3 environments running simultaneously: +# - Dev: http://localhost:8000 +# - Feature-auth: http://localhost:9000 +# - Feature-ui: http://localhost:10000 +``` + +### Testing While Developing + +```bash +# Terminal 1: Development environment +./start-env.sh dev + +# Terminal 2: Run tests in isolated environment +./start-env.sh test +# Tests run on ports 9000, 4010, etc. - no conflict with dev! +``` + +### Quick Feature Testing + +```bash +# Create environment config +cat > environments/quick-test.env <` +- **Isolated**: Different databases, ports, data dirs +- **Efficient**: Shared infrastructure +- **Flexible**: Override any config variable +- **Secure**: Secrets separated and gitignored +- **Git-friendly**: Perfect for worktrees +- **Clean**: Easy cleanup with `make env-clean` + +Start using it: + +```bash +# One-time setup: Create secrets file +cp .env.secrets.template .env.secrets +nano .env.secrets # Add your API keys + +# Create your environment +cp environments/dev.env environments/my-work.env +# Edit my-work.env with your settings + +# Start it +./start-env.sh my-work + +# Work on it +# http://localhost: + +# Clean up when done +make env-clean ENV=my-work +``` + +## Files Reference + +| File | Purpose | Edit? | Git? | +|------|---------|-------|------| +| `docker-defaults.env` | System infrastructure URLs | Rarely | โœ… Yes | +| `config-docker.env` | **User settings** (what you change) | **Often** | โœ… Yes | +| `config-k8s.env` | Kubernetes configuration | As needed | โœ… Yes | +| `config.env` | Config router / documentation | No | โœ… Yes | +| `.env.secrets` | **API keys and passwords** | **Always** | โŒ No | +| `.env.secrets.template` | Template for secrets | No | โœ… Yes | +| `environments/dev.env` | Dev environment overrides | As needed | โœ… Yes | +| `environments/test.env` | Test environment overrides | As needed | โœ… Yes | +| `environments/*.env` | Custom environment overrides | As needed | โœ… Yes | +| `backends/advanced/.env.{name}` | Generated backend config | Never | โŒ No | diff --git a/Makefile b/Makefile index 3d03a180..984549f1 100644 --- a/Makefile +++ b/Makefile @@ -4,22 +4,36 @@ # Central management interface for Friend-Lite project # Handles configuration, deployment, and maintenance tasks -# Load environment variables from .env file +# Load environment variables from .env file (if it exists) ifneq (,$(wildcard ./.env)) include .env export $(shell sed 's/=.*//' .env | grep -v '^\s*$$' | grep -v '^\s*\#') endif -# Load configuration definitions -include config.env -# Export all variables from config.env -export $(shell sed 's/=.*//' config.env | grep -v '^\s*$$' | grep -v '^\s*\#') +# Load configuration definitions for Kubernetes +# Use config-k8s.env for K8s deployments +ifneq (,$(wildcard ./config-k8s.env)) + include config-k8s.env + export $(shell sed 's/=.*//' config-k8s.env | grep -v '^\s*$$' | grep -v '^\s*\#') +else + # Fallback to config.env for backwards compatibility + ifneq (,$(wildcard ./config.env)) + include config.env + export $(shell sed 's/=.*//' config.env | grep -v '^\s*$$' | grep -v '^\s*\#') + endif +endif + +# Load secrets (gitignored) - required for K8s secrets generation +ifneq (,$(wildcard ./.env.secrets)) + include .env.secrets + export $(shell sed 's/=.*//' .env.secrets | grep -v '^\s*$$' | grep -v '^\s*\#') +endif # Script directories SCRIPTS_DIR := scripts K8S_SCRIPTS_DIR := $(SCRIPTS_DIR)/k8s -.PHONY: help menu setup-k8s setup-infrastructure setup-rbac setup-storage-pvc config config-docker config-k8s config-all clean deploy deploy-docker deploy-k8s deploy-k8s-full deploy-infrastructure deploy-apps check-infrastructure check-apps build-backend up-backend down-backend k8s-status k8s-cleanup k8s-purge audio-manage mycelia-sync-status mycelia-sync-all mycelia-sync-user mycelia-check-orphans mycelia-reassign-orphans test-robot test-robot-integration test-robot-unit test-robot-endpoints test-robot-specific test-robot-clean +.PHONY: help menu wizard setup-secrets setup-tailscale setup-environment check-secrets setup-k8s setup-infrastructure setup-rbac setup-storage-pvc config config-docker config-k8s config-all clean deploy deploy-docker deploy-k8s deploy-k8s-full deploy-infrastructure deploy-apps check-infrastructure check-apps build-backend up-backend down-backend k8s-status k8s-cleanup k8s-purge audio-manage mycelia-sync-status mycelia-sync-all mycelia-sync-user mycelia-check-orphans mycelia-reassign-orphans test-robot test-robot-integration test-robot-unit test-robot-endpoints test-robot-specific test-robot-clean # Default target .DEFAULT_GOAL := menu @@ -28,6 +42,12 @@ menu: ## Show interactive menu (default) @echo "๐ŸŽฏ Friend-Lite Management System" @echo "================================" @echo + @echo "๐Ÿง™ Setup:" + @echo " wizard ๐Ÿง™ Interactive setup wizard (secrets + Tailscale + environment)" + @echo " setup-secrets ๐Ÿ” Configure API keys and passwords" + @echo " setup-tailscale ๐ŸŒ Configure Tailscale for distributed deployment" + @echo " setup-environment ๐Ÿ“ฆ Create a custom environment" + @echo @echo "๐Ÿ“‹ Quick Actions:" @echo " setup-dev ๐Ÿ› ๏ธ Setup development environment (git hooks, pre-commit)" @echo " setup-k8s ๐Ÿ—๏ธ Complete Kubernetes setup (registry + infrastructure + RBAC)" @@ -152,6 +172,441 @@ setup-dev: ## Setup development environment (git hooks, pre-commit) @echo "" @echo "โš™๏ธ To skip hooks: git push --no-verify / git commit --no-verify" +# ======================================== +# INTERACTIVE SETUP WIZARD +# ======================================== + +.PHONY: wizard setup-secrets setup-tailscale setup-environment check-secrets + +wizard: ## ๐Ÿง™ Interactive setup wizard - guides through complete Friend-Lite setup + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "๐Ÿง™ Friend-Lite Setup Wizard" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "" + @echo "This wizard will guide you through:" + @echo " 1. ๐Ÿ” Setting up secrets (API keys, passwords)" + @echo " 2. ๐ŸŒ Optionally configuring Tailscale for distributed deployment" + @echo " 3. ๐Ÿ“ฆ Creating a custom environment" + @echo " 4. ๐Ÿš€ Starting your Friend-Lite instance" + @echo "" + @read -p "Press Enter to continue or Ctrl+C to exit..." + @echo "" + @$(MAKE) --no-print-directory setup-secrets + @echo "" + @$(MAKE) --no-print-directory setup-tailscale + @echo "" + @$(MAKE) --no-print-directory setup-environment + @echo "" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "โœ… Setup Complete!" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "" + @echo "๐Ÿš€ Next Steps:" + @echo "" + @if [ -f ".env.secrets" ] && [ -d "environments" ]; then \ + echo " Start your environment:"; \ + echo " ./start-env.sh $${ENV_NAME:-dev}"; \ + echo ""; \ + echo " Or with optional services:"; \ + echo " ./start-env.sh $${ENV_NAME:-dev} --profile mycelia"; \ + echo " ./start-env.sh $${ENV_NAME:-dev} --profile speaker"; \ + else \ + echo " โš ๏ธ Some setup steps were skipped. Run individual targets:"; \ + echo " make setup-secrets"; \ + echo " make setup-environment"; \ + fi + @echo "" + @echo "๐Ÿ“š Documentation:" + @echo " โ€ข ENVIRONMENTS.md - Environment system overview" + @echo " โ€ข SSL_SETUP.md - Tailscale and SSL configuration" + @echo " โ€ข SETUP.md - Detailed setup instructions" + @echo "" + +check-secrets: ## Check if secrets file exists and is configured + @if [ ! -f ".env.secrets" ]; then \ + echo "โŒ .env.secrets not found"; \ + exit 1; \ + fi + @if ! grep -q "^AUTH_SECRET_KEY=" .env.secrets || grep -q "your-super-secret" .env.secrets; then \ + echo "โŒ .env.secrets exists but needs configuration"; \ + exit 1; \ + fi + @echo "โœ… Secrets file configured" + +setup-secrets: ## ๐Ÿ” Interactive secrets setup (API keys, passwords) + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "๐Ÿ” Step 1: Secrets Configuration" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "" + @if [ -f ".env.secrets" ]; then \ + echo "โ„น๏ธ .env.secrets already exists"; \ + echo ""; \ + read -p "Do you want to reconfigure it? (y/N): " reconfigure; \ + if [ "$$reconfigure" != "y" ] && [ "$$reconfigure" != "Y" ]; then \ + echo ""; \ + echo "โœ… Keeping existing secrets"; \ + exit 0; \ + fi; \ + echo ""; \ + echo "๐Ÿ“ Backing up existing .env.secrets..."; \ + cp .env.secrets .env.secrets.backup.$$(date +%Y%m%d_%H%M%S); \ + echo ""; \ + else \ + echo "๐Ÿ“ Creating .env.secrets from template..."; \ + cp .env.secrets.template .env.secrets; \ + echo "โœ… Created .env.secrets"; \ + echo ""; \ + fi + @echo "๐Ÿ”‘ Required Secrets Configuration" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "" + @echo "Let's configure your secrets. Press Enter to skip optional ones." + @echo "" + @# JWT Secret Key (required) + @echo "1๏ธโƒฃ JWT Secret Key (required for authentication)" + @echo " This is used to sign JWT tokens. Should be random and secure." + @read -p " Enter JWT secret key (or press Enter to generate): " jwt_key; \ + if [ -z "$$jwt_key" ]; then \ + jwt_key=$$(openssl rand -hex 32 2>/dev/null || cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 64 | head -n 1); \ + echo " โœ… Generated random key: $$jwt_key"; \ + fi; \ + sed -i.bak "s|^AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=$$jwt_key|" .env.secrets && rm .env.secrets.bak + @echo "" + @# Admin credentials + @echo "2๏ธโƒฃ Admin Account" + @read -p " Admin email (default: admin@example.com): " admin_email; \ + admin_email=$${admin_email:-admin@example.com}; \ + sed -i.bak "s|^ADMIN_EMAIL=.*|ADMIN_EMAIL=$$admin_email|" .env.secrets && rm .env.secrets.bak; \ + read -sp " Admin password: " admin_pass; echo ""; \ + if [ -n "$$admin_pass" ]; then \ + sed -i.bak "s|^ADMIN_PASSWORD=.*|ADMIN_PASSWORD=$$admin_pass|" .env.secrets && rm .env.secrets.bak; \ + fi + @echo "" + @# OpenAI API Key + @echo "3๏ธโƒฃ OpenAI API Key (required for memory extraction)" + @echo " Get your key from: https://platform.openai.com/api-keys" + @read -p " OpenAI API key (or press Enter to skip): " openai_key; \ + if [ -n "$$openai_key" ]; then \ + sed -i.bak "s|^OPENAI_API_KEY=.*|OPENAI_API_KEY=$$openai_key|" .env.secrets && rm .env.secrets.bak; \ + fi + @echo "" + @# Deepgram API Key + @echo "4๏ธโƒฃ Deepgram API Key (recommended for transcription)" + @echo " Get your key from: https://console.deepgram.com/" + @read -p " Deepgram API key (or press Enter to skip): " deepgram_key; \ + if [ -n "$$deepgram_key" ]; then \ + sed -i.bak "s|^DEEPGRAM_API_KEY=.*|DEEPGRAM_API_KEY=$$deepgram_key|" .env.secrets && rm .env.secrets.bak; \ + fi + @echo "" + @# Optional: Mistral API Key + @echo "5๏ธโƒฃ Mistral API Key (optional - alternative transcription)" + @echo " Get your key from: https://console.mistral.ai/" + @read -p " Mistral API key (or press Enter to skip): " mistral_key; \ + if [ -n "$$mistral_key" ]; then \ + sed -i.bak "s|^MISTRAL_API_KEY=.*|MISTRAL_API_KEY=$$mistral_key|" .env.secrets && rm .env.secrets.bak; \ + fi + @echo "" + @# Optional: Hugging Face Token + @echo "6๏ธโƒฃ Hugging Face Token (optional - for speaker recognition models)" + @echo " Get your token from: https://huggingface.co/settings/tokens" + @read -p " HF token (or press Enter to skip): " hf_token; \ + if [ -n "$$hf_token" ]; then \ + sed -i.bak "s|^HF_TOKEN=.*|HF_TOKEN=$$hf_token|" .env.secrets && rm .env.secrets.bak; \ + fi + @echo "" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "โœ… Secrets configured successfully!" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "" + @echo "๐Ÿ“„ Configuration saved to: .env.secrets" + @echo "๐Ÿ”’ This file is gitignored and will not be committed" + @echo "" + +setup-tailscale: ## ๐ŸŒ Interactive Tailscale setup for distributed deployment + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "๐ŸŒ Step 2: Tailscale Configuration (Optional)" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "" + @echo "Tailscale enables secure distributed deployments:" + @echo " โ€ข Run services on different machines" + @echo " โ€ข Secure service-to-service communication" + @echo " โ€ข Access from mobile devices" + @echo " โ€ข Automatic HTTPS with 'tailscale serve'" + @echo "" + @read -p "Do you want to configure Tailscale? (y/N): " use_tailscale; \ + if [ "$$use_tailscale" != "y" ] && [ "$$use_tailscale" != "Y" ]; then \ + echo ""; \ + echo "โ„น๏ธ Skipping Tailscale setup"; \ + echo " You can run this later with: make setup-tailscale"; \ + exit 0; \ + fi + @echo "" + @# Check if Tailscale is installed + @if ! command -v tailscale >/dev/null 2>&1; then \ + echo "โŒ Tailscale not found"; \ + echo ""; \ + echo "๐Ÿ“ฆ Install Tailscale:"; \ + echo " curl -fsSL https://tailscale.com/install.sh | sh"; \ + echo " sudo tailscale up"; \ + echo ""; \ + echo "Then run this setup again: make setup-tailscale"; \ + exit 1; \ + fi + @echo "โœ… Tailscale is installed" + @echo "" + @# Get Tailscale status + @echo "๐Ÿ“Š Checking Tailscale status..." + @if ! tailscale status >/dev/null 2>&1; then \ + echo "โŒ Tailscale is not running"; \ + echo ""; \ + echo "๐Ÿ”ง Start Tailscale:"; \ + echo " sudo tailscale up"; \ + echo ""; \ + exit 1; \ + fi + @echo "โœ… Tailscale is running" + @echo "" + @echo "๐Ÿ“‹ Your Tailscale devices:" + @echo "" + @tailscale status | head -n 10 + @echo "" + @# Get Tailscale hostname + @echo "๐Ÿท๏ธ Tailscale Hostname Configuration" + @echo "" + @echo "Your Tailscale hostname is the DNS name assigned to THIS machine." + @echo "It's different from the IP address - it's a permanent name." + @echo "" + @echo "๐Ÿ“‹ To find your Tailscale hostname:" + @echo " 1. Run: tailscale status" + @echo " 2. Look for this machine's name in the first column" + @echo " 3. The full hostname is shown on the right (ends in .ts.net)" + @echo "" + @echo "Example output:" + @echo " anubis 100.x.x.x anubis.tail12345.ts.net <-- Your hostname" + @echo "" + @default_hostname=$$(tailscale status --json 2>/dev/null | grep -o '"DNSName":"[^"]*"' | head -1 | cut -d'"' -f4 | sed 's/\.$$//'); \ + if [ -n "$$default_hostname" ]; then \ + echo "๐Ÿ’ก Auto-detected hostname for THIS machine: $$default_hostname"; \ + echo ""; \ + fi; \ + read -p "Tailscale hostname [$$default_hostname]: " tailscale_hostname; \ + tailscale_hostname=$${tailscale_hostname:-$$default_hostname}; \ + if [ -z "$$tailscale_hostname" ]; then \ + echo ""; \ + echo "โŒ No hostname provided"; \ + exit 1; \ + fi; \ + export TAILSCALE_HOSTNAME=$$tailscale_hostname; \ + echo ""; \ + echo "โœ… Using Tailscale hostname: $$tailscale_hostname" + @echo "" + @# SSL Setup + @echo "๐Ÿ” SSL Certificate Configuration" + @echo "" + @echo "How do you want to handle HTTPS?" + @echo " 1) Use 'tailscale serve' (automatic HTTPS, recommended)" + @echo " 2) Generate self-signed certificates" + @echo " 3) Skip SSL setup" + @echo "" + @read -p "Choose option (1-3) [1]: " ssl_choice; \ + ssl_choice=$${ssl_choice:-1}; \ + case $$ssl_choice in \ + 1) \ + echo ""; \ + echo "โœ… Will use 'tailscale serve' for automatic HTTPS"; \ + echo ""; \ + echo "๐Ÿ“ After starting services, run:"; \ + echo " tailscale serve https / http://localhost:8000"; \ + echo " tailscale serve https / http://localhost:5173"; \ + echo ""; \ + export HTTPS_ENABLED=true; \ + ;; \ + 2) \ + echo ""; \ + echo "๐Ÿ” Generating SSL certificates for $$tailscale_hostname..."; \ + if [ -f "backends/advanced/ssl/generate-ssl.sh" ]; then \ + cd backends/advanced && ./ssl/generate-ssl.sh $$tailscale_hostname && cd ../..; \ + echo ""; \ + echo "โœ… SSL certificates generated"; \ + else \ + echo "โŒ SSL generation script not found"; \ + exit 1; \ + fi; \ + export HTTPS_ENABLED=true; \ + ;; \ + 3) \ + echo ""; \ + echo "โ„น๏ธ Skipping SSL setup"; \ + export HTTPS_ENABLED=false; \ + ;; \ + *) \ + echo ""; \ + echo "โŒ Invalid choice"; \ + exit 1; \ + ;; \ + esac + @echo "" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "โœ… Tailscale configuration complete!" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "" + +setup-environment: ## ๐Ÿ“ฆ Create a custom environment configuration + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "๐Ÿ“ฆ Step 3: Environment Setup" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "" + @echo "Environments allow you to:" + @echo " โ€ข Run multiple isolated instances (dev, staging, prod)" + @echo " โ€ข Use different databases and ports for each" + @echo " โ€ข Test changes without affecting production" + @echo "" + @# Check existing environments + @if [ -d "environments" ] && [ -n "$$(ls -A environments/*.env 2>/dev/null)" ]; then \ + echo "๐Ÿ“‹ Existing environments:"; \ + ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$$||' | sed 's/^/ - /'; \ + echo ""; \ + fi + @# Get environment name + @read -p "Environment name [dev]: " env_name; \ + env_name=$${env_name:-dev}; \ + mkdir -p environments; \ + env_file="environments/$$env_name.env"; \ + echo ""; \ + if [ -f "$$env_file" ]; then \ + echo "โš ๏ธ Environment '$$env_name' already exists"; \ + read -p "Do you want to overwrite it? (y/N): " overwrite; \ + if [ "$$overwrite" != "y" ] && [ "$$overwrite" != "Y" ]; then \ + echo ""; \ + echo "โ„น๏ธ Keeping existing environment"; \ + exit 0; \ + fi; \ + echo ""; \ + cp "$$env_file" "$$env_file.backup.$$(date +%Y%m%d_%H%M%S)"; \ + echo "๐Ÿ“ Backed up existing environment"; \ + echo ""; \ + fi + @# Get port offset + @echo "๐Ÿ”ข Port Configuration"; \ + echo ""; \ + echo "Each environment needs a unique port offset to avoid conflicts."; \ + echo " dev: 0 (8000, 5173, 27017, ...)"; \ + echo " staging: 100 (8100, 5273, 27117, ...)"; \ + echo " prod: 200 (8200, 5373, 27217, ...)"; \ + echo ""; \ + read -p "Port offset [0]: " port_offset; \ + port_offset=$${port_offset:-0}; \ + echo "" + @# Get database names + @echo "๐Ÿ’พ Database Configuration"; \ + echo ""; \ + read -p "MongoDB database name [friend-lite-$$env_name]: " mongodb_db; \ + mongodb_db=$${mongodb_db:-friend-lite-$$env_name}; \ + read -p "Mycelia database name [mycelia-$$env_name]: " mycelia_db; \ + mycelia_db=$${mycelia_db:-mycelia-$$env_name}; \ + echo "" + @# Optional services + @echo "๐Ÿ”Œ Optional Services"; \ + echo ""; \ + read -p "Enable Mycelia? (y/N): " enable_mycelia; \ + read -p "Enable Speaker Recognition? (y/N): " enable_speaker; \ + read -p "Enable OpenMemory MCP? (y/N): " enable_openmemory; \ + read -p "Enable Parakeet ASR? (y/N): " enable_parakeet; \ + services=""; \ + if [ "$$enable_mycelia" = "y" ] || [ "$$enable_mycelia" = "Y" ]; then \ + services="$$services mycelia"; \ + fi; \ + if [ "$$enable_speaker" = "y" ] || [ "$$enable_speaker" = "Y" ]; then \ + services="$$services speaker"; \ + fi; \ + if [ "$$enable_openmemory" = "y" ] || [ "$$enable_openmemory" = "Y" ]; then \ + services="$$services openmemory"; \ + fi; \ + if [ "$$enable_parakeet" = "y" ] || [ "$$enable_parakeet" = "Y" ]; then \ + services="$$services parakeet"; \ + fi; \ + echo "" + @# Tailscale settings (from previous step or ask) + @if [ -n "$$TAILSCALE_HOSTNAME" ]; then \ + echo ""; \ + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"; \ + echo "๐ŸŒ Tailscale Configuration"; \ + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"; \ + echo ""; \ + echo "โœ… Using Tailscale configuration from previous step:"; \ + echo " Hostname: $$TAILSCALE_HOSTNAME"; \ + echo " HTTPS: $$HTTPS_ENABLED"; \ + echo ""; \ + tailscale_hostname=$$TAILSCALE_HOSTNAME; \ + https_enabled=$$HTTPS_ENABLED; \ + else \ + echo ""; \ + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"; \ + echo "๐ŸŒ Tailscale Configuration (Optional)"; \ + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"; \ + echo ""; \ + echo "โš ๏ธ You skipped Tailscale setup earlier."; \ + echo ""; \ + echo "You can still configure it for this environment:"; \ + echo " โ€ข Enter your Tailscale hostname (from 'tailscale status')"; \ + echo " โ€ข Or press Enter to skip (HTTP only, no Tailscale)"; \ + echo ""; \ + read -p "Tailscale hostname (or press Enter to skip): " tailscale_hostname; \ + if [ -n "$$tailscale_hostname" ]; then \ + echo ""; \ + echo "โš ๏ธ Note: SSL certificates were not generated."; \ + echo " To generate them later, run:"; \ + echo " cd backends/advanced && ./ssl/generate-ssl.sh $$tailscale_hostname"; \ + echo ""; \ + https_enabled=true; \ + else \ + https_enabled=false; \ + fi; \ + fi; \ + echo "" + @# Write environment file + @echo "๐Ÿ“ Creating environment file: $$env_file"; \ + echo ""; \ + printf "# ========================================\n" > "$$env_file"; \ + printf "# Friend-Lite Environment: %s\n" "$$env_name" >> "$$env_file"; \ + printf "# ========================================\n" >> "$$env_file"; \ + printf "# Generated: %s\n" "$$(date)" >> "$$env_file"; \ + printf "\n" >> "$$env_file"; \ + printf "# Environment identification\n" >> "$$env_file"; \ + printf "ENV_NAME=%s\n" "$$env_name" >> "$$env_file"; \ + printf "COMPOSE_PROJECT_NAME=friend-lite-%s\n" "$$env_name" >> "$$env_file"; \ + printf "\n" >> "$$env_file"; \ + printf "# Port offset (each environment needs unique ports)\n" >> "$$env_file"; \ + printf "PORT_OFFSET=%s\n" "$$port_offset" >> "$$env_file"; \ + printf "\n" >> "$$env_file"; \ + printf "# Data directory (isolated per environment)\n" >> "$$env_file"; \ + printf "DATA_DIR=./data/%s\n" "$$env_name" >> "$$env_file"; \ + printf "\n" >> "$$env_file"; \ + printf "# Database names (isolated per environment)\n" >> "$$env_file"; \ + printf "MONGODB_DATABASE=%s\n" "$$mongodb_db" >> "$$env_file"; \ + printf "MYCELIA_DB=%s\n" "$$mycelia_db" >> "$$env_file"; \ + printf "\n" >> "$$env_file"; \ + printf "# Optional services\n" >> "$$env_file"; \ + printf "SERVICES=%s\n" "$$services" >> "$$env_file"; \ + printf "\n" >> "$$env_file"; \ + if [ -n "$$tailscale_hostname" ]; then \ + printf "# Tailscale configuration\n" >> "$$env_file"; \ + printf "TAILSCALE_HOSTNAME=%s\n" "$$tailscale_hostname" >> "$$env_file"; \ + printf "HTTPS_ENABLED=%s\n" "$$https_enabled" >> "$$env_file"; \ + printf "\n" >> "$$env_file"; \ + fi; \ + echo "โœ… Environment created: $$env_name" + @echo "" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "โœ… Environment setup complete!" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "" + @echo "๐Ÿ“„ Environment file: $$env_file" + @echo "" + @echo "๐Ÿš€ Start this environment with:" + @echo " ./start-env.sh $$env_name" + @echo "" + # ======================================== # KUBERNETES SETUP # ======================================== @@ -428,3 +883,61 @@ test-robot-clean: ## Clean up Robot Framework test results @echo "๐Ÿงน Cleaning up Robot Framework test results..." @rm -rf results/ @echo "โœ… Test results cleaned" + +# ======================================== +# MULTI-ENVIRONMENT SUPPORT +# ======================================== + +env-list: ## List available environments + @echo "๐Ÿ“‹ Available Environments:" + @echo "" + @ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$$||' | while read env; do \ + echo " โ€ข $$env"; \ + if [ -f "environments/$$env.env" ]; then \ + grep '^# ' environments/$$env.env | head -1 | sed 's/^# / /'; \ + fi; \ + done + @echo "" + @echo "Usage: make env-start ENV=" + @echo " or: ./start-env.sh [options]" + +env-start: ## Start specific environment (usage: make env-start ENV=dev) + @if [ -z "$(ENV)" ]; then \ + echo "โŒ ENV parameter required"; \ + echo "Usage: make env-start ENV=dev"; \ + echo ""; \ + $(MAKE) env-list; \ + exit 1; \ + fi + @./start-env.sh $(ENV) $(OPTS) + +env-stop: ## Stop specific environment (usage: make env-stop ENV=dev) + @if [ -z "$(ENV)" ]; then \ + echo "โŒ ENV parameter required"; \ + echo "Usage: make env-stop ENV=dev"; \ + exit 1; \ + fi + @echo "๐Ÿ›‘ Stopping environment: $(ENV)" + @COMPOSE_PROJECT_NAME=friend-lite-$(ENV) docker compose down + +env-clean: ## Clean specific environment data (usage: make env-clean ENV=dev) + @if [ -z "$(ENV)" ]; then \ + echo "โŒ ENV parameter required"; \ + echo "Usage: make env-clean ENV=dev"; \ + exit 1; \ + fi + @echo "โš ๏ธ This will delete all data for environment: $(ENV)" + @read -p "Continue? (y/N): " confirm && [ "$$confirm" = "y" ] || exit 1 + @source environments/$(ENV).env && rm -rf $$DATA_DIR + @COMPOSE_PROJECT_NAME=friend-lite-$(ENV) docker compose down -v + @echo "โœ… Environment $(ENV) cleaned" + +env-status: ## Show status of all environments + @echo "๐Ÿ“Š Environment Status:" + @echo "" + @for env in $$(ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$$||'); do \ + echo "Environment: $$env"; \ + COMPOSE_PROJECT_NAME=friend-lite-$$env docker compose ps 2>/dev/null | grep -v "NAME" || echo " Not running"; \ + echo ""; \ + done + diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 00000000..55125e11 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,218 @@ +# Friend-Lite Setup Guide + +Quick setup guide for getting Friend-Lite running with Docker Compose. + +## Prerequisites + +- Docker and Docker Compose installed +- Git (if cloning from repository) + +## Initial Setup + +### 1. Set Up Secrets + +Copy the secrets template and add your credentials: + +```bash +cp .env.secrets.template .env.secrets +nano .env.secrets # or use your preferred editor +``` + +**Required secrets:** + +```bash +# Authentication +AUTH_SECRET_KEY=your-super-secret-jwt-key-change-this +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=your-secure-password + +# LLM (for memory extraction) +OPENAI_API_KEY=sk-your-openai-key-here + +# Transcription +DEEPGRAM_API_KEY=your-deepgram-key-here + +# Optional: Speaker Recognition +HF_TOKEN=hf_your_huggingface_token +``` + +**โš ๏ธ Important**: Never commit `.env.secrets` - it's gitignored for security. + +### 2. Review Configuration (Optional) + +Configuration is split into two files: + +**`config-docker.env`** - User settings (what you change): +```bash +# LLM provider (openai, ollama, groq) +LLM_PROVIDER=openai +OPENAI_MODEL=gpt-4o-mini + +# Transcription provider (deepgram, mistral, parakeet) +TRANSCRIPTION_PROVIDER=deepgram + +# Memory provider (friend_lite, openmemory_mcp) +MEMORY_PROVIDER=friend_lite +``` + +**`docker-defaults.env`** - System constants (rarely change): +- Infrastructure URLs (`mongodb://mongo:27017`) +- Service names and ports +- Only edit if using external services + +## Starting Friend-Lite + +### Single Environment (Default) + +Start the default development environment: + +```bash +./start-env.sh dev +``` + +Access at: +- **Web UI**: http://localhost:3010 +- **Backend API**: http://localhost:8000 +- **API Docs**: http://localhost:8000/docs + +### With Optional Services + +Start with Mycelia memory interface: + +```bash +./start-env.sh dev --profile mycelia +``` + +Start with speaker recognition: + +```bash +./start-env.sh dev --profile speaker +``` + +Combine multiple profiles: + +```bash +./start-env.sh dev --profile mycelia --profile speaker +``` + +### Multiple Environments Simultaneously + +See [ENVIRONMENTS.md](ENVIRONMENTS.md) for detailed multi-environment setup. + +Quick example: + +```bash +# Terminal 1: Dev environment +./start-env.sh dev + +# Terminal 2: Test environment (different ports/database) +./start-env.sh test +``` + +## Stopping Services + +Press `Ctrl+C` in the terminal running the services, or: + +```bash +make env-stop ENV=dev +``` + +## Verifying Installation + +### Check Services + +```bash +# Health check +curl http://localhost:8000/health + +# Readiness (checks all dependencies) +curl http://localhost:8000/readiness +``` + +### Check Logs + +```bash +# All services +docker compose logs + +# Specific service +docker compose logs friend-backend + +# Follow logs +docker compose logs -f friend-backend +``` + +### Login to Web UI + +1. Open http://localhost:3010 +2. Use credentials from `.env.secrets`: + - Email: `ADMIN_EMAIL` + - Password: `ADMIN_PASSWORD` + +## Troubleshooting + +### "env.secrets not found" Warning + +Create the secrets file: + +```bash +cp .env.secrets.template .env.secrets +nano .env.secrets # Add your credentials +``` + +### Port Conflicts + +If ports are already in use, edit `environments/dev.env`: + +```bash +PORT_OFFSET=1000 # Changes ports to 9000, 4010, etc. +``` + +### Service Won't Start + +Check logs: + +```bash +docker compose logs +``` + +Common issues: +- Missing secrets in `.env.secrets` +- Invalid API keys +- Insufficient Docker resources (increase memory limit) + +### Database Issues + +Reset the database (โš ๏ธ deletes all data): + +```bash +make env-clean ENV=dev +./start-env.sh dev +``` + +## Next Steps + +- **[ENVIRONMENTS.md](ENVIRONMENTS.md)** - Multi-environment management +- **[CLAUDE.md](CLAUDE.md)** - Complete project documentation +- **Backend Docs**: http://localhost:8000/docs (when running) +- **API Reference**: [docs/api-reference.md](docs/api-reference.md) + +## Configuration Files Reference + +| File | Purpose | You Edit? | Git? | +|------|---------|-----------|------| +| `.env.secrets` | **Your API keys and passwords** | โœ… Always | โŒ No | +| `.env.secrets.template` | Template for secrets | No | โœ… Yes | +| `config-docker.env` | **User settings** (providers, models) | โœ… Often | โœ… Yes | +| `docker-defaults.env` | System infrastructure URLs | Rarely | โœ… Yes | +| `config-k8s.env` | Kubernetes configuration | As needed | โœ… Yes | +| `config.env` | Config router (documentation) | No | โœ… Yes | +| `environments/dev.env` | Environment overrides | As needed | โœ… Yes | +| `docker-compose.yml` | Service definitions | No | โœ… Yes | + +## Support + +For issues and questions: +- Check logs: `docker compose logs ` +- Review [CLAUDE.md](CLAUDE.md) for detailed documentation +- Check [ENVIRONMENTS.md](ENVIRONMENTS.md) for environment setup diff --git a/WIZARD.md b/WIZARD.md new file mode 100644 index 00000000..e802f3d4 --- /dev/null +++ b/WIZARD.md @@ -0,0 +1,483 @@ +# Friend-Lite Setup Wizard + +The Friend-Lite setup wizard provides an interactive, step-by-step guide to configure your Friend-Lite instance. + +## Quick Start + +```bash +make wizard +``` + +This single command will guide you through: +1. ๐Ÿ” **Secrets Configuration** - API keys and passwords +2. ๐ŸŒ **Tailscale Setup** - Distributed deployment (optional) +3. ๐Ÿ“ฆ **Environment Creation** - Custom isolated environment +4. ๐Ÿš€ **Start Instructions** - How to launch your instance + +## What Gets Configured + +### 1. Secrets (.env.secrets) + +The wizard creates and configures `.env.secrets` with: + +**Required:** +- `AUTH_SECRET_KEY` - JWT signing key (auto-generated if not provided) +- `ADMIN_EMAIL` - Admin account email +- `ADMIN_PASSWORD` - Admin account password +- `OPENAI_API_KEY` - For memory extraction and LLM features + +**Recommended:** +- `DEEPGRAM_API_KEY` - For speech-to-text transcription + +**Optional:** +- `MISTRAL_API_KEY` - Alternative transcription provider +- `HF_TOKEN` - Hugging Face token for speaker recognition models + +**Security:** +- File is automatically gitignored +- Backups created before modifications +- Sensitive data never committed to repository + +### 2. Tailscale Configuration (Optional) + +If you choose to configure Tailscale: + +**Checks performed:** +- โœ… Tailscale is installed +- โœ… Tailscale is running +- โœ… Your Tailscale devices are listed + +**Configuration:** +- Auto-detects your Tailscale hostname +- Offers three SSL/TLS options: + 1. **Tailscale Serve** - Automatic HTTPS (recommended) + 2. **Self-signed certificates** - Generated for your hostname + 3. **Skip SSL** - HTTP only (development) + +**SSL Certificate Generation:** +- Creates certificates with SANs for your Tailscale hostname +- Certificates valid for 365 days +- Stored in `backends/advanced/ssl/` + +### 3. Environment Creation + +Creates isolated environment in `environments/.env` with: + +**Environment Settings:** +- `ENV_NAME` - Unique identifier +- `COMPOSE_PROJECT_NAME` - Docker Compose project name +- `PORT_OFFSET` - Unique port offset to avoid conflicts + +**Database Isolation:** +- `MONGODB_DATABASE` - Separate MongoDB database +- `MYCELIA_DB` - Separate Mycelia database + +**Optional Services:** +- Mycelia (memory management UI) +- Speaker Recognition +- OpenMemory MCP +- Parakeet ASR (offline transcription) + +**Tailscale Integration:** +- `TAILSCALE_HOSTNAME` - Your Tailscale hostname +- `HTTPS_ENABLED` - SSL/TLS enabled flag + +## Individual Setup Commands + +You can run each step independently: + +### Configure Secrets + +```bash +make setup-secrets +``` + +**Interactive prompts for:** +- JWT secret key (or auto-generate) +- Admin email and password +- API keys (OpenAI, Deepgram, Mistral, HF) + +**Handles existing files:** +- Detects existing `.env.secrets` +- Offers to reconfigure or keep existing +- Creates timestamped backups + +### Configure Tailscale + +```bash +make setup-tailscale +``` + +**Validates:** +- Tailscale installation +- Tailscale running status +- Available devices + +**Configures:** +- Hostname detection and confirmation +- SSL/TLS method selection +- Certificate generation (if option 2 selected) + +### Create Environment + +```bash +make setup-environment +``` + +**Prompts for:** +- Environment name (default: dev) +- Port offset (default: 0) +- Database names (defaults: `friend-lite-`, `mycelia-`) +- Optional services to enable +- Tailscale hostname (if not already set) + +**Creates:** +- Environment file in `environments/.env` +- Timestamped backups of existing environments + +## Example Workflows + +### Workflow 1: Local Development (No Tailscale) + +```bash +make wizard +``` + +**Choices:** +1. Configure secrets โ†’ Yes (provide API keys) +2. Configure Tailscale โ†’ No +3. Environment name โ†’ `dev` +4. Port offset โ†’ `0` +5. Optional services โ†’ None + +**Result:** +```bash +./start-env.sh dev +# Services available at http://localhost:8000 and http://localhost:5173 +``` + +### Workflow 2: Distributed Deployment with Tailscale + +```bash +make wizard +``` + +**Choices:** +1. Configure secrets โ†’ Yes (provide API keys) +2. Configure Tailscale โ†’ Yes + - SSL option โ†’ 1 (Tailscale Serve) +3. Environment name โ†’ `prod` +4. Port offset โ†’ `0` +5. Optional services โ†’ Mycelia, Speaker Recognition + +**Result:** +```bash +./start-env.sh prod + +# After services start: +tailscale serve https / http://localhost:8000 +tailscale serve https / http://localhost:5173 + +# Services available at https://your-hostname.tailxxxxx.ts.net +``` + +### Workflow 3: Multiple Environments + +```bash +# Create dev environment +make wizard +# Choose: dev, port offset 0 + +# Create staging environment +make setup-environment +# Choose: staging, port offset 100 + +# Create prod environment +make setup-environment +# Choose: prod, port offset 200 + +# Run multiple environments simultaneously +./start-env.sh dev & +./start-env.sh staging & +./start-env.sh prod & +``` + +## Port Allocation + +Each environment uses a unique port offset: + +| Environment | Offset | Backend | WebUI | MongoDB | Redis | Qdrant | +|-------------|--------|---------|-------|---------|-------|--------| +| dev | 0 | 8000 | 5173 | 27017 | 6379 | 6333 | +| staging | 100 | 8100 | 5273 | 27117 | 6479 | 6433 | +| prod | 200 | 8200 | 5373 | 27217 | 6579 | 6533 | + +## Environment File Structure + +Generated environment file (`environments/.env`): + +```bash +# ======================================== +# Friend-Lite Environment: dev +# ======================================== +# Generated: 2025-01-23 10:30:00 + +# Environment identification +ENV_NAME=dev +COMPOSE_PROJECT_NAME=friend-lite-dev + +# Port offset (each environment needs unique ports) +PORT_OFFSET=0 + +# Data directory (isolated per environment) +DATA_DIR=./data/dev + +# Database names (isolated per environment) +MONGODB_DATABASE=friend-lite-dev +MYCELIA_DB=mycelia-dev + +# Optional services +SERVICES=mycelia speaker + +# Tailscale configuration (if configured) +TAILSCALE_HOSTNAME=friend-lite.tailxxxxx.ts.net +HTTPS_ENABLED=true +``` + +## Configuration Files Overview + +After running the wizard, your configuration structure: + +``` +friend-lite/ +โ”œโ”€โ”€ .env.secrets # Secrets (gitignored) +โ”œโ”€โ”€ .env.secrets.template # Template for secrets +โ”œโ”€โ”€ config-docker.env # Docker Compose user settings +โ”œโ”€โ”€ docker-defaults.env # Docker Compose system defaults +โ”œโ”€โ”€ config-k8s.env # Kubernetes configuration +โ”œโ”€โ”€ environments/ # Environment-specific configs +โ”‚ โ”œโ”€โ”€ dev.env # Development environment +โ”‚ โ”œโ”€โ”€ staging.env # Staging environment +โ”‚ โ””โ”€โ”€ prod.env # Production environment +โ””โ”€โ”€ backends/advanced/ + โ”œโ”€โ”€ ssl/ # SSL certificates (if generated) + โ”‚ โ”œโ”€โ”€ server.crt + โ”‚ โ””โ”€โ”€ server.key + โ””โ”€โ”€ .env -> .env.dev # Symlink to active environment +``` + +## Checking Configuration Status + +```bash +# Check if secrets are configured +make check-secrets + +# View wizard help +make help | grep -A 20 "SETUP WIZARD" + +# List existing environments +ls -1 environments/*.env | sed 's|environments/||;s|.env$||' +``` + +## Tailscale Hostname Confusion? + +If you're confused about what to enter for "Tailscale hostname", see **[`TAILSCALE_GUIDE.md`](TAILSCALE_GUIDE.md)** for a detailed explanation. + +**Quick answer:** Run `tailscale status` and use the **third column** (ends in `.ts.net`) + +Example: +``` +anubis 100.83.66.30 anubis.tail12345.ts.net linux - + ^^^^^^^^^^^^^^^^^^^^^^^^ + Use this! +``` + +## Troubleshooting + +### Issue: "openssl: command not found" + +**Cause:** OpenSSL not installed (needed for JWT key generation) + +**Solution:** +```bash +# macOS +brew install openssl + +# Ubuntu/Debian +sudo apt-get install openssl + +# Or provide your own JWT key when prompted +``` + +### Issue: "tailscale: command not found" + +**Cause:** Tailscale not installed + +**Solution:** +```bash +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up +``` + +### Issue: "Tailscale is not running" + +**Cause:** Tailscale installed but not started + +**Solution:** +```bash +sudo tailscale up +``` + +### Issue: "Cannot create .env.secrets: Permission denied" + +**Cause:** Insufficient permissions + +**Solution:** +```bash +# Ensure you're in the project root +cd /path/to/friend-lite + +# Check file permissions +ls -la .env.secrets.template + +# Fix if needed +chmod 644 .env.secrets.template +``` + +### Issue: "Port already in use" + +**Cause:** Another environment or service using the same ports + +**Solution:** +- Use a different port offset (100, 200, etc.) +- Stop conflicting services +- Check running environments: `docker ps` + +## Advanced Usage + +### Running Wizard Non-Interactively + +While the wizard is designed to be interactive, you can prepare files ahead of time: + +```bash +# 1. Create .env.secrets manually +cp .env.secrets.template .env.secrets +# Edit .env.secrets with your values + +# 2. Create environment file manually +mkdir -p environments +cat > environments/dev.env <> environments/dev.env + +# Restart environment to apply changes +./start-env.sh dev +``` + +### Regenerating SSL Certificates + +```bash +# For a specific Tailscale hostname +cd backends/advanced +./ssl/generate-ssl.sh your-hostname.tailxxxxx.ts.net + +# Or run Tailscale setup again +cd ../.. +make setup-tailscale +``` + +## Integration with Existing Setup + +The wizard works alongside existing configuration: + +**Preserves:** +- Existing `.env.secrets` (asks before overwriting) +- Existing environments (asks before overwriting) +- `config-docker.env` and `config-k8s.env` (not modified) + +**Creates:** +- `.env.secrets` (if missing) +- Environment-specific configs in `environments/` +- SSL certificates in `backends/advanced/ssl/` (if requested) + +**Does not modify:** +- `config-docker.env` - Manual user settings +- `docker-defaults.env` - System defaults +- `config-k8s.env` - Kubernetes configuration + +## Next Steps After Wizard + +1. **Start your environment:** + ```bash + ./start-env.sh + ``` + +2. **Access services:** + - Backend API: `http://localhost:8000` (or your Tailscale URL) + - Web UI: `http://localhost:5173` (or your Tailscale URL) + +3. **If using Tailscale Serve:** + ```bash + tailscale serve https / http://localhost:8000 + tailscale serve https / http://localhost:5173 + ``` + +4. **Check service health:** + ```bash + curl http://localhost:8000/health + ``` + +5. **View logs:** + ```bash + docker compose logs -f friend-backend + ``` + +6. **Explore documentation:** + - `ENVIRONMENTS.md` - Environment system details + - `SSL_SETUP.md` - SSL/TLS configuration + - `SETUP.md` - Complete setup guide + +## Wizard vs Manual Setup + +| Aspect | Wizard (`make wizard`) | Manual Setup | +|--------|----------------------|--------------| +| Speed | ๐ŸŸข 5-10 minutes | ๐ŸŸก 15-30 minutes | +| Errors | ๐ŸŸข Guided validation | ๐Ÿ”ด Manual validation needed | +| Documentation | ๐ŸŸข Auto-generates configs | ๐ŸŸก Must read docs | +| Flexibility | ๐ŸŸก Standard options | ๐ŸŸข Full customization | +| Best For | First-time setup, quick start | Advanced users, custom needs | + +## Summary + +The Friend-Lite wizard provides a streamlined, interactive setup experience: + +โœ… Guides through all configuration steps +โœ… Validates inputs and system requirements +โœ… Creates timestamped backups +โœ… Supports both local and distributed deployments +โœ… Integrates with Tailscale for secure networking +โœ… Can be run in parts or as a complete flow +โœ… Preserves existing configurations + +Run `make wizard` to get started in minutes! diff --git a/backends/advanced/compose/backend.yml b/backends/advanced/compose/backend.yml new file mode 100644 index 00000000..195b3f40 --- /dev/null +++ b/backends/advanced/compose/backend.yml @@ -0,0 +1,74 @@ +# Backend Services +# Friend-Lite backend API and workers + +services: + friend-backend: + build: + context: .. + dockerfile: Dockerfile + ports: + - "${BACKEND_PORT:-8000}:8000" + env_file: + - ../.env + volumes: + - ../src:/app/src + - ../data/audio_chunks:/app/audio_chunks + - ../data/debug_dir:/app/debug_dir + - ../data:/app/data + environment: + # Service URLs (Docker internal network) + - REDIS_URL=redis://redis:6379/0 + - MYCELIA_URL=${MYCELIA_URL:-http://mycelia-backend:5173} + # Complex defaults + - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3010,http://localhost:3015,http://localhost:3020,http://localhost:8000} + depends_on: + qdrant: + condition: service_started + mongo: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/readiness"] + interval: 30s + timeout: 30s + retries: 5 + start_period: 5s + restart: unless-stopped + networks: + - chronicle-network + + # Unified Worker Container + # Runs all workers in a single container: + # - 3 RQ workers (transcription, memory, default queues) + # - 1 Audio stream worker (Redis Streams consumer - must be single instance) + workers: + build: + context: .. + dockerfile: Dockerfile + command: ["./start-workers.sh"] + env_file: + - ../.env + volumes: + - ../src:/app/src + - ../start-workers.sh:/app/start-workers.sh + - ../data/audio_chunks:/app/audio_chunks + - ../data:/app/data + environment: + # Service URLs (Docker internal network) + - REDIS_URL=redis://redis:6379/0 + depends_on: + redis: + condition: service_healthy + mongo: + condition: service_healthy + qdrant: + condition: service_started + restart: unless-stopped + networks: + - chronicle-network + +networks: + chronicle-network: + name: chronicle-network + external: true diff --git a/backends/advanced/compose/frontend.yml b/backends/advanced/compose/frontend.yml new file mode 100644 index 00000000..9c18868c --- /dev/null +++ b/backends/advanced/compose/frontend.yml @@ -0,0 +1,24 @@ +# Frontend Services +# Web UI for Friend-Lite + +services: + webui: + build: + context: ../webui + dockerfile: Dockerfile + args: + - VITE_BACKEND_URL=${VITE_BACKEND_URL:-http://localhost:8000} + - BACKEND_URL=${BACKEND_URL:-http://localhost:8000} + ports: + - "${WEBUI_PORT:-3010}:80" + depends_on: + friend-backend: + condition: service_healthy + restart: unless-stopped + networks: + - chronicle-network + +networks: + chronicle-network: + name: chronicle-network + external: true diff --git a/backends/advanced/compose/infrastructure.yml b/backends/advanced/compose/infrastructure.yml new file mode 100644 index 00000000..5d45474a --- /dev/null +++ b/backends/advanced/compose/infrastructure.yml @@ -0,0 +1,57 @@ +# Infrastructure Services +# Core database and cache services required by all environments + +services: + qdrant: + image: qdrant/qdrant:latest + ports: + - "${QDRANT_GRPC_PORT:-6033}:6333" + - "${QDRANT_HTTP_PORT:-6034}:6334" + volumes: + - ${QDRANT_DATA_PATH:-../data/qdrant_data}:/qdrant/storage + networks: + - infra-network + - chronicle-network + + mongo: + image: mongo:8.0.14 + ports: + - "${MONGO_PORT:-27017}:27017" + volumes: + - mongo_data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand({ ping: 1 })"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - infra-network + - chronicle-network + + redis: + image: redis:7-alpine + ports: + - "${REDIS_PORT:-6379}:6379" + volumes: + - ${REDIS_DATA_PATH:-../data/redis_data}:/data + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + networks: + - infra-network + - chronicle-network + +networks: + infra-network: + driver: bridge + chronicle-network: + name: chronicle-network + external: true + +volumes: + mongo_data: + driver: local diff --git a/backends/advanced/compose/mycelia.yml b/backends/advanced/compose/mycelia.yml new file mode 100644 index 00000000..eec877ba --- /dev/null +++ b/backends/advanced/compose/mycelia.yml @@ -0,0 +1,9 @@ +# Mycelia Services (Backend-Level Reference) +# NOTE: Mycelia is now defined at project root level (../../compose/mycelia.yml) +# This file kept for backward compatibility when running from backends/advanced/ +# +# Mycelia is at the same level as other extras (openmemory, speaker, asr) +# Use from project root: docker compose --profile mycelia up + +# Empty services - mycelia defined at root level +services: {} diff --git a/backends/advanced/compose/optional-services.yml b/backends/advanced/compose/optional-services.yml new file mode 100644 index 00000000..cb188820 --- /dev/null +++ b/backends/advanced/compose/optional-services.yml @@ -0,0 +1,116 @@ +# Optional Services +# Services that can be enabled via profiles or are commented out by default + +services: + # Caddy reverse proxy - provides HTTPS for microphone access + # Access at: https://localhost (accepts self-signed cert warning) + # Enable with: docker compose --profile https up + caddy: + image: caddy:2-alpine + ports: + - "443:443" + - "80:80" # HTTP redirect to HTTPS + volumes: + - ../Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + restart: unless-stopped + profiles: + - https + networks: + - chronicle-network + + # Ollama - Local LLM service + # Uncomment to use local LLM instead of OpenAI + # ollama: + # image: ollama/ollama:latest + # container_name: ollama + # ports: + # - "11434:11434" + # volumes: + # - ollama_data:/root/.ollama + # networks: + # - chronicle-network + # # Uncomment for GPU support: + # # deploy: + # # resources: + # # reservations: + # # devices: + # # - driver: nvidia + # # count: all + # # capabilities: [gpu] + + # Neo4j - Graph database for advanced memory relationships + # Uncomment if using neo4j-based memory provider + # neo4j-mem0: + # image: neo4j:5.15-community + # ports: + # - "7474:7474" # HTTP + # - "7687:7687" # Bolt + # environment: + # - NEO4J_AUTH=neo4j/${NEO4J_PASSWORD:-password} + # - NEO4J_PLUGINS=["apoc"] + # - NEO4J_dbms_security_procedures_unrestricted=apoc.* + # - NEO4J_dbms_security_procedures_allowlist=apoc.* + # volumes: + # - neo4j_data:/data + # - neo4j_logs:/logs + # restart: unless-stopped + # networks: + # - chronicle-network + + # OpenMemory MCP Server + # Uncomment if using openmemory_mcp memory provider + # openmemory-mcp: + # build: + # context: ../../extras/openmemory-mcp/cache/mem0/openmemory/api + # dockerfile: Dockerfile + # env_file: + # - .env + # environment: + # - OPENAI_API_KEY=${OPENAI_API_KEY} + # - OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory} + # depends_on: + # - qdrant + # ports: + # - "8765:8765" + # restart: unless-stopped + # healthcheck: + # test: ["CMD", "python", "-c", "import requests; exit(0 if requests.get('http://localhost:8765/docs').status_code == 200 else 1)"] + # interval: 30s + # timeout: 10s + # retries: 3 + # start_period: 30s + # networks: + # - chronicle-network + + # Ngrok - Expose to internet for testing + # UNCOMMENT FOR LOCAL DEMO - EXPOSES to internet + # Use Tailscale instead for production + # ngrok: + # image: ngrok/ngrok:latest + # depends_on: [friend-backend, proxy] + # ports: + # - "4040:4040" # Ngrok web interface + # environment: + # - NGROK_AUTHTOKEN=${NGROK_AUTHTOKEN} + # command: "http proxy:80 --url=${NGROK_URL} --basic-auth=${NGROK_BASIC_AUTH}" + # networks: + # - chronicle-network + +networks: + chronicle-network: + name: chronicle-network + external: true + +volumes: + caddy_data: + driver: local + caddy_config: + driver: local + ollama_data: + driver: local + neo4j_data: + driver: local + neo4j_logs: + driver: local diff --git a/backends/advanced/compose/overrides/dev.yml b/backends/advanced/compose/overrides/dev.yml new file mode 100644 index 00000000..545ee293 --- /dev/null +++ b/backends/advanced/compose/overrides/dev.yml @@ -0,0 +1,5 @@ +# Development Environment Overrides +# This file is intentionally minimal - dev settings are in base compose files +# Only add overrides here if they conflict with base or test environments + +services: {} diff --git a/backends/advanced/compose/overrides/prod.yml b/backends/advanced/compose/overrides/prod.yml new file mode 100644 index 00000000..f9196a93 --- /dev/null +++ b/backends/advanced/compose/overrides/prod.yml @@ -0,0 +1,82 @@ +# Production Environment Overrides +# Production-ready configuration with security and performance optimizations + +services: + friend-backend: + # Production: no source mounting + volumes: + - ./data/audio_chunks:/app/audio_chunks + - ./data/debug_dir:/app/debug_dir + - ./data:/app/data + # Remove source mount for production + environment: + # Production should use env-specific CORS + - CORS_ORIGINS=${CORS_ORIGINS} # Must be explicitly set in .env.prod + restart: always + # Production: Add resource limits + deploy: + resources: + limits: + cpus: '2' + memory: 4G + reservations: + cpus: '1' + memory: 2G + + workers: + # Production: no source mounting + volumes: + - ./data/audio_chunks:/app/audio_chunks + - ./data:/app/data + # Remove source mount for production + restart: always + deploy: + resources: + limits: + cpus: '4' + memory: 8G + reservations: + cpus: '2' + memory: 4G + + webui: + # Production: use production Dockerfile (already default) + restart: always + deploy: + resources: + limits: + cpus: '1' + memory: 512M + + qdrant: + restart: always + deploy: + resources: + limits: + cpus: '2' + memory: 4G + + mongo: + restart: always + deploy: + resources: + limits: + cpus: '2' + memory: 4G + + redis: + restart: always + deploy: + resources: + limits: + cpus: '1' + memory: 2G + + # Production: Mycelia without dev source mounts + mycelia-backend: + volumes: [] # Remove source mount + restart: always + + mycelia-frontend: + volumes: [] # Remove source mount + restart: always diff --git a/backends/advanced/compose/overrides/test.yml b/backends/advanced/compose/overrides/test.yml new file mode 100644 index 00000000..86ab99d5 --- /dev/null +++ b/backends/advanced/compose/overrides/test.yml @@ -0,0 +1,143 @@ +# Test Environment Overrides +# Isolated test environment for integration tests +# Uses different ports and databases to avoid conflicts with development + +services: + # Test infrastructure with isolated ports + qdrant: + ports: + - "6337:6333" # gRPC - avoid conflict with dev + - "6338:6334" # HTTP - avoid conflict with dev + volumes: + - ./data/test_qdrant_data:/qdrant/storage + + mongo: + ports: + - "27018:27017" # Avoid conflict with dev + volumes: + - ./data/test_mongo_data:/data/db + command: mongod --dbpath /data/db --bind_ip_all + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.runCommand('ping').ok", "--quiet"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 10s + + redis: + ports: + - "6380:6379" # Avoid conflict with dev + volumes: + - ./data/test_redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + # Test backend with test-specific configuration + friend-backend: + ports: + - "8001:8000" # Avoid conflict with dev + volumes: + - ./src:/app/src + - ./data/test_audio_chunks:/app/audio_chunks + - ./data/test_debug_dir:/app/debug_dir + - ./data/test_data:/app/data + environment: + # Test database connections + - MONGODB_URI=mongodb://mongo:27017/test_db + - QDRANT_BASE_URL=qdrant + - QDRANT_PORT=6333 + - REDIS_URL=redis://redis:6379/0 + - DEBUG_DIR=/app/debug_dir + # Test authentication + - AUTH_SECRET_KEY=test-jwt-signing-key-for-integration-tests + - ADMIN_PASSWORD=test-admin-password-123 + - ADMIN_EMAIL=test-admin@example.com + # Test provider configuration + - TRANSCRIPTION_PROVIDER=${TRANSCRIPTION_PROVIDER:-deepgram} + - MEMORY_PROVIDER=${MEMORY_PROVIDER:-friend_lite} + - MYCELIA_URL=http://mycelia-backend:5173 + - MYCELIA_DB=mycelia_test + # Test-specific settings + - DISABLE_SPEAKER_RECOGNITION=false + - SPEAKER_SERVICE_URL=https://localhost:8085 + - CORS_ORIGINS=http://localhost:3001,http://localhost:8001 + - SPEECH_INACTIVITY_THRESHOLD_SECONDS=2 # Fast timeout for tests + - WAIT_FOR_AUDIO_QUEUE_DRAIN=true + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/readiness"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + # Test workers + workers: + volumes: + - ./src:/app/src + - ./data/test_audio_chunks:/app/audio_chunks + - ./data/test_debug_dir:/app/debug_dir + - ./data/test_data:/app/data + environment: + # Same environment as test backend + - MONGODB_URI=mongodb://mongo:27017/test_db + - QDRANT_BASE_URL=qdrant + - QDRANT_PORT=6333 + - REDIS_URL=redis://redis:6379/0 + - DEBUG_DIR=/app/debug_dir + - AUTH_SECRET_KEY=test-jwt-signing-key-for-integration-tests + - ADMIN_PASSWORD=test-admin-password-123 + - ADMIN_EMAIL=test-admin@example.com + - TRANSCRIPTION_PROVIDER=${TRANSCRIPTION_PROVIDER:-deepgram} + - MEMORY_PROVIDER=${MEMORY_PROVIDER:-friend_lite} + - MYCELIA_URL=http://mycelia-backend:5173 + - MYCELIA_DB=mycelia_test + - DISABLE_SPEAKER_RECOGNITION=false + - SPEAKER_SERVICE_URL=https://localhost:8085 + - SPEECH_INACTIVITY_THRESHOLD_SECONDS=2 + - WAIT_FOR_AUDIO_QUEUE_DRAIN=true + + # Test webui + webui: + build: + args: + - VITE_BACKEND_URL=http://localhost:8001 + - BACKEND_URL=http://localhost:8001 + ports: + - "3001:80" # Avoid conflict with dev + volumes: + - ./webui/src:/app/src + + # Test Mycelia backend + mycelia-backend: + ports: + - "5100:5173" # Test backend port + environment: + # Test JWT secret + - JWT_SECRET=test-jwt-signing-key-for-integration-tests + - SECRET_KEY=test-jwt-signing-key-for-integration-tests + # Test database + - MONGO_URL=mongodb://mongo:27017 + - MONGO_DB=mycelia_test + - DATABASE_NAME=mycelia_test + # Test Redis + - REDIS_HOST=redis + - REDIS_PORT=6379 + healthcheck: + test: ["CMD", "deno", "eval", "fetch('http://localhost:5173/health').then(r => r.ok ? Deno.exit(0) : Deno.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + + # Test Mycelia frontend + mycelia-frontend: + build: + args: + - VITE_API_URL=http://localhost:5100 + ports: + - "3002:8080" + environment: + - VITE_API_URL=http://localhost:5100 diff --git a/backends/advanced/docker-compose.override.yml b/backends/advanced/docker-compose.override.yml new file mode 100644 index 00000000..497197c8 --- /dev/null +++ b/backends/advanced/docker-compose.override.yml @@ -0,0 +1,22 @@ +# Docker Compose override for development +# This file is automatically applied when running `docker compose up` +# It provides hot reload for the webui service + +# services: +# webui: +# build: +# context: ./webui +# dockerfile: Dockerfile.dev +# ports: +# - "300:5173" # Map to Vite dev server port +# volumes: +# # Mount source code for hot reload +# - ./webui/src:/app/src:ro +# - ./webui/public:/app/public:ro +# - ./webui/index.html:/app/index.html:ro +# - ./webui/vite.config.ts:/app/vite.config.ts:ro +# - ./webui/tsconfig.json:/app/tsconfig.json:ro +# - ./webui/tsconfig.node.json:/app/tsconfig.node.json:ro +# command: npm run dev -- --host 0.0.0.0 +# environment: +# - NODE_ENV=development diff --git a/backends/advanced/docker-compose.yml b/backends/advanced/docker-compose.yml index d9d58dca..d9f3eb51 100644 --- a/backends/advanced/docker-compose.yml +++ b/backends/advanced/docker-compose.yml @@ -1,242 +1,54 @@ -services: - friend-backend: - build: - context: . - dockerfile: Dockerfile - ports: - - "8000:8000" - env_file: - - .env - volumes: - - ./src:/app/src # Mount source code for development - - ./data/audio_chunks:/app/audio_chunks - - ./data/debug_dir:/app/debug_dir - - ./data:/app/data - environment: - - DEEPGRAM_API_KEY=${DEEPGRAM_API_KEY} - - MISTRAL_API_KEY=${MISTRAL_API_KEY} - - MISTRAL_MODEL=${MISTRAL_MODEL} - - TRANSCRIPTION_PROVIDER=${TRANSCRIPTION_PROVIDER} - - PARAKEET_ASR_URL=${PARAKEET_ASR_URL} - - OFFLINE_ASR_TCP_URI=${OFFLINE_ASR_TCP_URI} - - OLLAMA_BASE_URL=${OLLAMA_BASE_URL} - - HF_TOKEN=${HF_TOKEN} - - SPEAKER_SERVICE_URL=${SPEAKER_SERVICE_URL} - - ADMIN_PASSWORD=${ADMIN_PASSWORD} - - ADMIN_EMAIL=${ADMIN_EMAIL} - - AUTH_SECRET_KEY=${AUTH_SECRET_KEY} - - LLM_PROVIDER=${LLM_PROVIDER} - - OPENAI_API_KEY=${OPENAI_API_KEY} - - OPENAI_BASE_URL=${OPENAI_BASE_URL} - - OPENAI_MODEL=${OPENAI_MODEL} - - NEO4J_HOST=${NEO4J_HOST} - - NEO4J_USER=${NEO4J_USER} - - NEO4J_PASSWORD=${NEO4J_PASSWORD} - - CORS_ORIGINS=http://localhost:3010,http://localhost:8000,http://192.168.1.153:3010,http://192.168.1.153:8000,https://localhost:3010,https://localhost:8000,https://100.105.225.45,https://localhost - - REDIS_URL=redis://redis:6379/0 - depends_on: - qdrant: - condition: service_started - mongo: - condition: service_healthy - redis: - condition: service_healthy - # neo4j-mem0: - # condition: service_started - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/readiness"] - interval: 30s - timeout: 30s - retries: 5 - start_period: 5s - restart: unless-stopped - - # Unified Worker Container - # No CUDA needed for friend-backend and workers, workers only orchestrate jobs and call external services - # Runs all workers in a single container for efficiency: - # - 3 RQ workers (transcription, memory, default queues) - # - 1 Audio stream worker (Redis Streams consumer - must be single to maintain sequential chunks) - workers: - build: - context: . - dockerfile: Dockerfile - command: ["./start-workers.sh"] - env_file: - - .env - volumes: - - ./src:/app/src - - ./start-workers.sh:/app/start-workers.sh - - ./data/audio_chunks:/app/audio_chunks - - ./data:/app/data - environment: - - DEEPGRAM_API_KEY=${DEEPGRAM_API_KEY} - - MISTRAL_API_KEY=${MISTRAL_API_KEY} - - MISTRAL_MODEL=${MISTRAL_MODEL} - - TRANSCRIPTION_PROVIDER=${TRANSCRIPTION_PROVIDER} - - OPENAI_API_KEY=${OPENAI_API_KEY} - - OPENAI_BASE_URL=${OPENAI_BASE_URL} - - OPENAI_MODEL=${OPENAI_MODEL} - - LLM_PROVIDER=${LLM_PROVIDER} - - REDIS_URL=redis://redis:6379/0 - depends_on: - redis: - condition: service_healthy - mongo: - condition: service_healthy - qdrant: - condition: service_started - restart: unless-stopped - - webui: - build: - context: ./webui - dockerfile: Dockerfile - args: - # Direct access (http://localhost:3010): - # - VITE_BACKEND_URL=http://localhost:8000 - # - BACKEND_URL=http://localhost:8000 - # For Caddy HTTPS (https://localhost), use: - - VITE_BACKEND_URL= - - BACKEND_URL= - ports: - # - "${WEBUI_PORT:-3010}:80" - - 3010:80 - depends_on: - friend-backend: - condition: service_healthy - restart: unless-stopped - - # Caddy reverse proxy - provides HTTPS for microphone access - # Access at: https://localhost (accepts self-signed cert warning) - # Only starts when HTTPS is configured (Caddyfile exists) - caddy: - image: caddy:2-alpine - ports: - - "443:443" - - "80:80" # HTTP redirect to HTTPS - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - depends_on: - friend-backend: - condition: service_healthy - restart: unless-stopped - profiles: - - https - - # Development webui service (use with docker-compose --profile dev up) - webui-dev: - build: - context: ./webui - dockerfile: Dockerfile.dev - ports: - - "${WEBUI_DEV_PORT:-5173}:5173" - environment: - - VITE_BACKEND_URL=http://${HOST_IP}:${BACKEND_PUBLIC_PORT:-8000} - volumes: - - ./webui/src:/app/src - - ./webui/public:/app/public - depends_on: - friend-backend: - condition: service_healthy - profiles: - - dev - - qdrant: - image: qdrant/qdrant:latest - ports: - - "6033:6033" # gRPC - - "6034:6034" # HTTP - volumes: - - ./data/qdrant_data:/qdrant/storage - - - mongo: - image: mongo:8.0.14 - ports: - - "27017:27017" - volumes: - - mongo_data:/data/db - healthcheck: - test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand({ ping: 1 })"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - - redis: - image: redis:7-alpine - ports: - - "6379:6379" # Avoid conflict with dev on 6379 - volumes: - - ./data/redis_data:/data - command: redis-server --appendonly yes - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 5 - - ## Additional - - # neo4j-mem0: - # image: neo4j:5.15-community - # ports: - # - "7474:7474" # HTTP - # - "7687:7687" # Bolt - # environment: - # - NEO4J_AUTH=neo4j/${NEO4J_PASSWORD:-password} - # - NEO4J_PLUGINS=["apoc"] - # - NEO4J_dbms_security_procedures_unrestricted=apoc.* - # - NEO4J_dbms_security_procedures_allowlist=apoc.* - # volumes: - # - ./data/neo4j_data:/data - # - ./data/neo4j_logs:/logs - # restart: unless-stopped - - # ollama: - # image: ollama/ollama:latest - # container_name: ollama - # ports: - # - "11434:11434" - # volumes: - # - ollama_data:/root/.ollama - # deploy: - # resources: - # reservations: - # devices: - # - driver: nvidia - # count: all - # capabilities: [gpu] - - - - # Use tailscale instead - # UNCOMMENT OUT FOR LOCAL DEMO - EXPOSES to internet - # ngrok: - # image: ngrok/ngrok:latest - # depends_on: [friend-backend, proxy] - # ports: - # - "4040:4040" # Ngrok web interface - # environment: - # - NGROK_AUTHTOKEN=${NGROK_AUTHTOKEN} - # command: "http proxy:80 --url=${NGROK_URL} --basic-auth=${NGROK_BASIC_AUTH}" - -# Shared network for cross-project communication +# Friend-Lite Docker Compose +# Root compose file using modular includes +# +# Usage: +# Development: docker compose up +# With Mycelia: docker compose --profile mycelia up +# With HTTPS: docker compose --profile https up +# Testing: docker compose -f docker-compose.yml -f compose/overrides/test.yml up +# Production: docker compose -f docker-compose.yml -f compose/overrides/prod.yml up +# +# Structure: +# compose/infrastructure.yml - Core services (mongo, redis, qdrant) +# compose/backend.yml - Backend API and workers +# compose/frontend.yml - Web UI +# compose/mycelia.yml - Mycelia services (--profile mycelia) +# compose/optional-services.yml - Caddy, Ollama, etc. +# compose/overrides/ - Environment-specific overrides + +include: + # Core infrastructure (always included) + - compose/infrastructure.yml + + # Application services (always included) + - compose/backend.yml + - compose/frontend.yml + + # Optional services (profile-based) + - compose/optional-services.yml + + # Note: Mycelia moved to root level (../../compose/mycelia.yml) + # To use Mycelia, run from project root: docker compose --profile mycelia up + + # Development overrides (default) + - compose/overrides/dev.yml + +# Shared network configuration networks: - default: - name: friend-network + chronicle-network: + name: chronicle-network + external: true +# Shared volume configuration volumes: - ollama_data: - driver: local mongo_data: driver: local caddy_data: driver: local caddy_config: driver: local + ollama_data: + driver: local neo4j_data: driver: local neo4j_logs: diff --git a/backends/advanced/generate-caddyfile.sh b/backends/advanced/generate-caddyfile.sh new file mode 100644 index 00000000..bc2afc67 --- /dev/null +++ b/backends/advanced/generate-caddyfile.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Generate Caddyfile from template with Tailscale hostname support + +set -e + +TAILSCALE_HOSTNAME="${TAILSCALE_HOSTNAME:-}" + +if [ -z "$TAILSCALE_HOSTNAME" ]; then + # No Tailscale hostname - use localhost only + echo "๐Ÿ”ง Generating Caddyfile for localhost only" + sed 's/ TAILSCALE_IP//' Caddyfile.template > Caddyfile +else + # Include Tailscale hostname + echo "๐Ÿ”ง Generating Caddyfile for localhost and $TAILSCALE_HOSTNAME" + sed "s/TAILSCALE_IP/$TAILSCALE_HOSTNAME/" Caddyfile.template > Caddyfile +fi + +echo "โœ… Caddyfile generated successfully" diff --git a/backends/advanced/webui/Dockerfile b/backends/advanced/webui/Dockerfile index 3b2f28d8..b4eb8cad 100644 --- a/backends/advanced/webui/Dockerfile +++ b/backends/advanced/webui/Dockerfile @@ -1,5 +1,5 @@ # Multi-stage build for React app -FROM node:18-alpine AS build +FROM node:22-alpine AS build # Set working directory WORKDIR /app @@ -7,8 +7,8 @@ WORKDIR /app # Copy package files COPY package.json package-lock.json ./ -# Install dependencies -RUN npm ci +# Install dependencies (with legacy-peer-deps for react-gantt-timeline compatibility) +RUN npm install --legacy-peer-deps # Copy source code COPY . . diff --git a/backends/advanced/webui/Dockerfile.dev b/backends/advanced/webui/Dockerfile.dev index c7a11575..02dbcca6 100644 --- a/backends/advanced/webui/Dockerfile.dev +++ b/backends/advanced/webui/Dockerfile.dev @@ -1,4 +1,4 @@ -# Development Dockerfile for React app +# Development Dockerfile for React app with hot reload FROM node:18-alpine WORKDIR /app @@ -6,14 +6,11 @@ WORKDIR /app # Copy package files COPY package.json package-lock.json ./ -# Install dependencies -RUN npm ci +# Install dependencies (with legacy-peer-deps for react-gantt-timeline compatibility) +RUN npm install --legacy-peer-deps -# Copy source code -COPY . . +# Expose Vite dev server port +EXPOSE 3020 -# Expose port -EXPOSE 5173 - -# Start development server -CMD ["npm", "run", "dev"] \ No newline at end of file +# Start Vite dev server with host binding for Docker +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/backends/advanced/webui/index.html b/backends/advanced/webui/index.html index 9949e6e5..17580bc1 100644 --- a/backends/advanced/webui/index.html +++ b/backends/advanced/webui/index.html @@ -4,6 +4,7 @@ + Friend-Lite Dashboard diff --git a/backends/advanced/webui/package-lock.json b/backends/advanced/webui/package-lock.json index bde3b515..36dca0ac 100644 --- a/backends/advanced/webui/package-lock.json +++ b/backends/advanced/webui/package-lock.json @@ -8,16 +8,25 @@ "name": "friend-lite-webui", "version": "0.1.0", "dependencies": { + "@types/d3": "^7.4.3", "axios": "^1.6.2", "clsx": "^2.0.0", + "d3": "^7.9.0", + "frappe-gantt": "^1.0.4", "lucide-react": "^0.294.0", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.20.0" + "react-gantt-timeline": "^0.4.3", + "react-router-dom": "^6.20.0", + "react-vertical-timeline-component": "^3.5.3", + "xss": "^1.0.15" }, "devDependencies": { + "@types/frappe-gantt": "^0.9.0", "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", + "@types/react-vertical-timeline-component": "^3.3.6", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", @@ -48,7 +57,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -62,7 +70,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -77,7 +84,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -87,7 +93,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -118,7 +123,6 @@ "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" @@ -128,7 +132,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.0", @@ -141,11 +144,143 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0-beta.53.tgz", + "integrity": "sha512-HVsEm3wjSe3BCXWxnyqrTWWQAxvtHR35F4q84jS68aS8R3WfbOnFEwlqsrWX5quZL0ArR68REOWRDCyG+JBSlQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@babel/helper-call-delegate": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.0.0-beta.53.tgz", + "integrity": "sha512-kOdnk7nDkQsAM+fxhiN6sy0jNep5VN6jv7H8pbu2trW5ziopw+cwNxTkihLUAEC+gJU45WngJTZtjUMR/2Kckg==", + "license": "MIT", + "dependencies": { + "@babel/helper-hoist-variables": "7.0.0-beta.53", + "@babel/traverse": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-call-delegate/node_modules/@babel/code-frame": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.53.tgz", + "integrity": "sha512-6o6EnDfG+zQqfrYDLPc5kGp6+klZFFFqGucljRcUa7IZuTBpvALWG0O+7rtOGFF1sYhr4jBib995RvFuNFxDMw==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-call-delegate/node_modules/@babel/generator": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.53.tgz", + "integrity": "sha512-XnfdZ6oFVC4cE4+7jbEa1MLFSXrGY/SfSE6onUyyPSrRbjYs9sdrYKi/JgKGSJX65A8GFswHwWcBPCynfVEr5g==", + "license": "MIT", + "dependencies": { + "@babel/types": "7.0.0-beta.53", + "jsesc": "^2.5.1", + "lodash": "^4.17.5", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "node_modules/@babel/helper-call-delegate/node_modules/@babel/parser": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.53.tgz", + "integrity": "sha512-SYoyLjcE+D28Ly2kkPXP6eIVy4YwViRSffri5WHi8PRxy8ngnx6mTXFzGAsSSPzUN3DK+sf8qBsdDGeQz1SJEw==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-call-delegate/node_modules/@babel/traverse": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.53.tgz", + "integrity": "sha512-JZh3vX/9ox9aoub2gLlpPRm8LM0yJuqzmp5MrbwD57SPh1dHMDWjGen9exbaITAe03t9MJV5PAacv0K2UJBffg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.0.0-beta.53", + "@babel/generator": "7.0.0-beta.53", + "@babel/helper-function-name": "7.0.0-beta.53", + "@babel/helper-split-export-declaration": "7.0.0-beta.53", + "@babel/parser": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "debug": "^3.1.0", + "globals": "^11.1.0", + "invariant": "^2.2.0", + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/helper-call-delegate/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@babel/helper-call-delegate/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@babel/helper-call-delegate/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-call-delegate/node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-call-delegate/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.2", @@ -162,27 +297,161 @@ "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/@babel/helper-define-map": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.0.0-beta.53.tgz", + "integrity": "sha512-rqDAadUz9cLul+epYez/X6PPwS85j/xL2q61JT14MFJaaCFKmQ8QhmZmktcSsYC8XhsDlaLAdwxSIw1a8oih9g==", + "license": "MIT", + "dependencies": { + "@babel/helper-function-name": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/helper-define-map/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.53.tgz", + "integrity": "sha512-vmdaNg17OWa0lFVJqZLQcvc59KIOcJDpyvqr3EJT9BYsjh/JxDlYq/JpBzLpWv9AkXeBdY4NevZXD37gdsLu0Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-get-function-arity": "7.0.0-beta.53", + "@babel/template": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/code-frame": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.53.tgz", + "integrity": "sha512-6o6EnDfG+zQqfrYDLPc5kGp6+klZFFFqGucljRcUa7IZuTBpvALWG0O+7rtOGFF1sYhr4jBib995RvFuNFxDMw==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/parser": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.53.tgz", + "integrity": "sha512-SYoyLjcE+D28Ly2kkPXP6eIVy4YwViRSffri5WHi8PRxy8ngnx6mTXFzGAsSSPzUN3DK+sf8qBsdDGeQz1SJEw==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/template": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.53.tgz", + "integrity": "sha512-MCZLPfGfNBHdE5wNfY5eK1hpY3fyq8zq+NfbfFCUtIzHl7SfUzHzH8rKPBXSB2Ypetq2sBHdDyslSSgnG0Watg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.0.0-beta.53", + "@babel/parser": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.53.tgz", + "integrity": "sha512-jLbME3MfCVT88GLuUDJ1X+ErDeWi59aeBb/O6pyhp5C+eVRRiLxzptRmpvJqG+Va6aOBWSoJ8uBNKJ1ghT/ONg==", + "license": "MIT", + "dependencies": { + "@babel/types": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-get-function-arity/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0-beta.53.tgz", + "integrity": "sha512-ktLEBpVZkvPUjNn8JK11m/74cWw9H9U3QizAiJUPdnvkvz/F0ucMyIOpMa7vuhmpHmlRzgAraIlTrmQfxv6BGg==", + "license": "MIT", + "dependencies": { + "@babel/types": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0-beta.53.tgz", + "integrity": "sha512-/rJvy0+ipYwZh5pXSrifbUo7Ct+Dfm85AQqSYphbX67qEOEk92phxE95Tpw1wtLgWEbWBQ3WRHfTyEadqlPocg==", + "license": "MIT", + "dependencies": { + "@babel/types": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -196,7 +465,6 @@ "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -210,21 +478,237 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0-beta.53.tgz", + "integrity": "sha512-B6DMEnC9slZtBDRRjLi7OTcfmsXPPZsRLldqQ0TZjWj4QuZWFSDlonVWIYI+2Fb9DiA/dZMXMv9JDgVGGibMkw==", + "license": "MIT", + "dependencies": { + "@babel/types": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-regex": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.0.0-beta.53.tgz", + "integrity": "sha512-Wh9ORGs15i37YovmEcS2W8PQDMR9T5UxAL1EtHW/uAfqH4T703dRK/7rsREYjA2lQ8pPHJWN/y0tMuCoOvl3jQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.0.0-beta.53.tgz", + "integrity": "sha512-/KG2wmojlGgtuco35Aq5RYftukhXiql4dG7ux+oAnpi6wALb6BjPmUWJe5m57lNNQgLtCfQ8596j0h5GZu65QA==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "7.0.0-beta.53", + "@babel/helper-optimise-call-expression": "7.0.0-beta.53", + "@babel/traverse": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/code-frame": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.53.tgz", + "integrity": "sha512-6o6EnDfG+zQqfrYDLPc5kGp6+klZFFFqGucljRcUa7IZuTBpvALWG0O+7rtOGFF1sYhr4jBib995RvFuNFxDMw==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/generator": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.53.tgz", + "integrity": "sha512-XnfdZ6oFVC4cE4+7jbEa1MLFSXrGY/SfSE6onUyyPSrRbjYs9sdrYKi/JgKGSJX65A8GFswHwWcBPCynfVEr5g==", + "license": "MIT", + "dependencies": { + "@babel/types": "7.0.0-beta.53", + "jsesc": "^2.5.1", + "lodash": "^4.17.5", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/parser": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.53.tgz", + "integrity": "sha512-SYoyLjcE+D28Ly2kkPXP6eIVy4YwViRSffri5WHi8PRxy8ngnx6mTXFzGAsSSPzUN3DK+sf8qBsdDGeQz1SJEw==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/traverse": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.53.tgz", + "integrity": "sha512-JZh3vX/9ox9aoub2gLlpPRm8LM0yJuqzmp5MrbwD57SPh1dHMDWjGen9exbaITAe03t9MJV5PAacv0K2UJBffg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.0.0-beta.53", + "@babel/generator": "7.0.0-beta.53", + "@babel/helper-function-name": "7.0.0-beta.53", + "@babel/helper-split-export-declaration": "7.0.0-beta.53", + "@babel/parser": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "debug": "^3.1.0", + "globals": "^11.1.0", + "invariant": "^2.2.0", + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.0.0-beta.53.tgz", + "integrity": "sha512-mq4csKX0vucrhZKgTG/ogNCuq6KiLEVXRDG5sRWggpuN4N6f/z+CyGNi83tqLRv9VLjV7IEQu/6UyI2wAUxFOg==", + "license": "MIT", + "dependencies": { + "@babel/template": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/helper-simple-access/node_modules/@babel/code-frame": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.53.tgz", + "integrity": "sha512-6o6EnDfG+zQqfrYDLPc5kGp6+klZFFFqGucljRcUa7IZuTBpvALWG0O+7rtOGFF1sYhr4jBib995RvFuNFxDMw==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-simple-access/node_modules/@babel/parser": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.53.tgz", + "integrity": "sha512-SYoyLjcE+D28Ly2kkPXP6eIVy4YwViRSffri5WHi8PRxy8ngnx6mTXFzGAsSSPzUN3DK+sf8qBsdDGeQz1SJEw==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-simple-access/node_modules/@babel/template": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.53.tgz", + "integrity": "sha512-MCZLPfGfNBHdE5wNfY5eK1hpY3fyq8zq+NfbfFCUtIzHl7SfUzHzH8rKPBXSB2Ypetq2sBHdDyslSSgnG0Watg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.0.0-beta.53", + "@babel/parser": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/helper-simple-access/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.53.tgz", + "integrity": "sha512-7twjNOXZFIuiGpfkaf2j1WuGFbfrmHS5ES9GXXXT0xbQ5UmyX9nvaTJHMt11t6pvIjv1xvtBVuDyMCrvyd+E/w==", + "license": "MIT", + "dependencies": { + "@babel/types": "7.0.0-beta.53" + } + }, + "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -234,7 +718,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -244,7 +727,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -254,7 +736,6 @@ "version": "7.28.2", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -264,797 +745,1058 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dev": true, + "node_modules/@babel/highlight": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.53.tgz", + "integrity": "sha512-5wvZd8RHAOzmTJ5bpupKM6x5OWXlViUK5ACDAUn7YXDd/JqQQZXi0CxDb8pH5IFV79mt6r5A/bZ/+NLhxpcZ5g==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^3.0.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "color-convert": "^1.9.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=4" } }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=4" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=4" } }, - "node_modules/@babel/traverse": { + "node_modules/@babel/parser": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", - "dev": true, + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", - "debug": "^4.3.1" + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=6.9.0" + "node": ">=6.0.0" } }, - "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "dev": true, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@babel/core": "^7.0.0-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, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0-beta.53.tgz", + "integrity": "sha512-CxYIFD+eutA6WzT16fsxGn8Y1A50iG3JVOeL8MGn51h42E2ea5xvfWnT/aAthEuKqbHR+FDyxBZ7o2lXRMwAag==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "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/@babel/plugin-transform-arrow-functions/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" }, - "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, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.0.0-beta.53.tgz", + "integrity": "sha512-bBCniYwYf4HI9jaVT1sf272aijJBDg17hAAAa1XduB/humwRD+XE5nZXYfh5ktMTjxHyWxB55LK6Y1W+F4RSDg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "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, + "node_modules/@babel/plugin-transform-block-scoped-functions/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.0.0-beta.53.tgz", + "integrity": "sha512-lBDSm853wqigbxRR9m71Ow91whK/gjOJzFo2kFwJulUZJaV/pdQGSXopANPRjITplKbDufIbClPdIK1V6dnN+w==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53", + "lodash": "^4.17.5" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "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, + "node_modules/@babel/plugin-transform-block-scoping/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.0.0-beta.53.tgz", + "integrity": "sha512-Mv/3NVDJ40aawBuwg+Yy776Qynmo8FRM4RLtGk+TyIH9PKw83b1jL0Gxa1OvzXjBiizq6oQLOhUvWnmh1uSL5A==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-annotate-as-pure": "7.0.0-beta.53", + "@babel/helper-define-map": "7.0.0-beta.53", + "@babel/helper-function-name": "7.0.0-beta.53", + "@babel/helper-optimise-call-expression": "7.0.0-beta.53", + "@babel/helper-plugin-utils": "7.0.0-beta.53", + "@babel/helper-replace-supers": "7.0.0-beta.53", + "@babel/helper-split-export-declaration": "7.0.0-beta.53", + "globals": "^11.1.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "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, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=4" } }, - "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, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.0.0-beta.53.tgz", + "integrity": "sha512-5q7Epq0AIj3Pakj1w4WeJvcGZbPpIGymjXjinh7s/U8nCuXsxxjBiBXc0Hk0IaY95C51FxWmmbp3t1v7QqifNw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "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, + "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.0.0-beta.53.tgz", + "integrity": "sha512-V7qWHCE5f2/hb4gd7rFe1MIkVoC0ZIo/XFR9YAHbRdUQtOI57FUjJL+Fz2BT5MaZYglfWFZy+YLnVzQhInJFJA==", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "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, + "node_modules/@babel/plugin-transform-destructuring/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.0.0-beta.53.tgz", + "integrity": "sha512-Laqs9gb/pkNVCZBugOYRhz8qaxz+XMz9CjKpF9eMrET1FToh0I2lp4yRKUHc4VPHd+oolkWvN55Hi28Ia3QOHQ==", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "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, + "node_modules/@babel/plugin-transform-duplicate-keys/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0-beta.53.tgz", + "integrity": "sha512-7DD7gd/ywy3fTBZvQ8CiulD3SsUZLNrw22zD4nmH7LS8mTFcUnAsenbzHMDKmFs02ZwkLaAOS0lAQZFuSA/bNQ==", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "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, + "node_modules/@babel/plugin-transform-for-of/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.0.0-beta.53.tgz", + "integrity": "sha512-mvkWR0ay5U8IZQiCRV02jnhk0uY+DHZCbmBVQ9KAYq0mvunmtxHsk3NtEzSTHMSyXr69BUkQlVnYlWtYN0HP6w==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-function-name": "7.0.0-beta.53", + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "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, + "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-instanceof": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-instanceof/-/plugin-transform-instanceof-7.0.0-beta.53.tgz", + "integrity": "sha512-/WX7rfrMy2nYfSGWdST3RZTPREDpNUNzl5YGx3O/M33qqepbeWBCwSggTSkdQ7iBAPi/CBjY7fGSVGCcCPR7/w==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "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, + "node_modules/@babel/plugin-transform-instanceof/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.0.0-beta.53.tgz", + "integrity": "sha512-1bWy5iRSQngH9klvojOdMotFH9PWY6aRDWSiHddIsc54VQrKz9NH6bBAwhf+2Jt+SJfCUbAaGuleeZkfMPZ7fg==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, + "node_modules/@babel/plugin-transform-literals/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.0.0-beta.53.tgz", + "integrity": "sha512-DldT9fmUfjr+pY1/fLidnMq4wk2GiN4114oWshYHSd5Eachi5BkfM6Ao2CsTbDL5PyTy3rdIRB6K9nL+7ze0YQ==", "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "@babel/helper-module-transforms": "7.0.0-beta.53", + "@babel/helper-plugin-utils": "7.0.0-beta.53" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, + "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/code-frame": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.53.tgz", + "integrity": "sha512-6o6EnDfG+zQqfrYDLPc5kGp6+klZFFFqGucljRcUa7IZuTBpvALWG0O+7rtOGFF1sYhr4jBib995RvFuNFxDMw==", "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "dependencies": { + "@babel/highlight": "7.0.0-beta.53" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, + "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-module-imports": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.53.tgz", + "integrity": "sha512-nyyERQH7kRCy0OR2Ek0+sD+wxZEhCmaLAVE7SylPYmCce1Dq8XGmibT1eQVekRkr78utXnDKMe4A269SBVlIRA==", "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-module-transforms": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.0.0-beta.53.tgz", + "integrity": "sha512-FQrR3poCdkIxIl+QGkw9Fq3fYcEmcFloO/CSX26FYZuXcHZ5FbPLZajtdcQmPNWWqIicHzCXd0h+gkcRSP9siQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "7.0.0-beta.53", + "@babel/helper-simple-access": "7.0.0-beta.53", + "@babel/helper-split-export-declaration": "7.0.0-beta.53", + "@babel/template": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/parser": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.53.tgz", + "integrity": "sha512-SYoyLjcE+D28Ly2kkPXP6eIVy4YwViRSffri5WHi8PRxy8ngnx6mTXFzGAsSSPzUN3DK+sf8qBsdDGeQz1SJEw==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=6.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/template": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.53.tgz", + "integrity": "sha512-MCZLPfGfNBHdE5wNfY5eK1hpY3fyq8zq+NfbfFCUtIzHl7SfUzHzH8rKPBXSB2Ypetq2sBHdDyslSSgnG0Watg==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@babel/code-frame": "7.0.0-beta.53", + "@babel/parser": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", + "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.0.0-beta.53.tgz", + "integrity": "sha512-PP1obXrhqknxHDtJ90DoB+SwjwLzC0bGCeYBAx1T0rgLjmWYMGB103wNpfdJ1jqUH5mmC6PTNKBlasTGiEc9Cw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "7.0.0-beta.53", + "@babel/helper-plugin-utils": "7.0.0-beta.53", + "@babel/helper-simple-access": "7.0.0-beta.53" }, - "engines": { - "node": "*" + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/code-frame": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.53.tgz", + "integrity": "sha512-6o6EnDfG+zQqfrYDLPc5kGp6+klZFFFqGucljRcUa7IZuTBpvALWG0O+7rtOGFF1sYhr4jBib995RvFuNFxDMw==", "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "dependencies": { + "@babel/highlight": "7.0.0-beta.53" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-module-imports": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.53.tgz", + "integrity": "sha512-nyyERQH7kRCy0OR2Ek0+sD+wxZEhCmaLAVE7SylPYmCce1Dq8XGmibT1eQVekRkr78utXnDKMe4A269SBVlIRA==", + "license": "MIT", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-module-transforms": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.0.0-beta.53.tgz", + "integrity": "sha512-FQrR3poCdkIxIl+QGkw9Fq3fYcEmcFloO/CSX26FYZuXcHZ5FbPLZajtdcQmPNWWqIicHzCXd0h+gkcRSP9siQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "7.0.0-beta.53", + "@babel/helper-simple-access": "7.0.0-beta.53", + "@babel/helper-split-export-declaration": "7.0.0-beta.53", + "@babel/template": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/parser": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.53.tgz", + "integrity": "sha512-SYoyLjcE+D28Ly2kkPXP6eIVy4YwViRSffri5WHi8PRxy8ngnx6mTXFzGAsSSPzUN3DK+sf8qBsdDGeQz1SJEw==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=10.10.0" + "node": ">=6.0.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/template": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.53.tgz", + "integrity": "sha512-MCZLPfGfNBHdE5wNfY5eK1hpY3fyq8zq+NfbfFCUtIzHl7SfUzHzH8rKPBXSB2Ypetq2sBHdDyslSSgnG0Watg==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@babel/code-frame": "7.0.0-beta.53", + "@babel/parser": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" } }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.0.0-beta.53.tgz", + "integrity": "sha512-ZCJZ18qEd7zf9F7Caph/z4TUARMlZiC5aI67wzOSeNpLhqSOrno5qxN7uukOuYvpsk3ji04S4tJOqHSuG+NUTA==", + "license": "MIT", + "dependencies": { + "@babel/helper-hoist-variables": "7.0.0-beta.53", + "@babel/helper-plugin-utils": "7.0.0-beta.53" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" + "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.0.0-beta.53.tgz", + "integrity": "sha512-TXX2R9rZQJbxyJ2leae4N+pT73t6Niolosa0WyZpLTGBmjKrFRHm/vpCm3v+tqb0GZMXbMQ/vzCRlMcQD8tPTQ==", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@babel/helper-module-transforms": "7.0.0-beta.53", + "@babel/helper-plugin-utils": "7.0.0-beta.53" }, - "engines": { - "node": ">=12" + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, + "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/code-frame": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.53.tgz", + "integrity": "sha512-6o6EnDfG+zQqfrYDLPc5kGp6+klZFFFqGucljRcUa7IZuTBpvALWG0O+7rtOGFF1sYhr4jBib995RvFuNFxDMw==", "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "dependencies": { + "@babel/highlight": "7.0.0-beta.53" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, + "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-module-imports": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.53.tgz", + "integrity": "sha512-nyyERQH7kRCy0OR2Ek0+sD+wxZEhCmaLAVE7SylPYmCce1Dq8XGmibT1eQVekRkr78utXnDKMe4A269SBVlIRA==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" } }, - "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, + "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-module-transforms": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.0.0-beta.53.tgz", + "integrity": "sha512-FQrR3poCdkIxIl+QGkw9Fq3fYcEmcFloO/CSX26FYZuXcHZ5FbPLZajtdcQmPNWWqIicHzCXd0h+gkcRSP9siQ==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@babel/helper-module-imports": "7.0.0-beta.53", + "@babel/helper-simple-access": "7.0.0-beta.53", + "@babel/helper-split-export-declaration": "7.0.0-beta.53", + "@babel/template": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" } }, - "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, + "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/parser": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.53.tgz", + "integrity": "sha512-SYoyLjcE+D28Ly2kkPXP6eIVy4YwViRSffri5WHi8PRxy8ngnx6mTXFzGAsSSPzUN3DK+sf8qBsdDGeQz1SJEw==", "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, "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/@babel/plugin-transform-modules-umd/node_modules/@babel/template": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.53.tgz", + "integrity": "sha512-MCZLPfGfNBHdE5wNfY5eK1hpY3fyq8zq+NfbfFCUtIzHl7SfUzHzH8rKPBXSB2Ypetq2sBHdDyslSSgnG0Watg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.0.0-beta.53", + "@babel/parser": "7.0.0-beta.53", + "@babel/types": "7.0.0-beta.53", + "lodash": "^4.17.5" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, + "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/types": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.53.tgz", + "integrity": "sha512-iL3DSWjQ890rA97uR5F1PhGtYniVGjqaRoRZtLz76bZhNNqmALftafrUnuJNzWC9z0eoaNcAtk7ZT/26mW/6Tg==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "esutils": "^2.0.2", + "lodash": "^4.17.5", + "to-fast-properties": "^2.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.0.0-beta.53.tgz", + "integrity": "sha512-ko8FH0QnK76kZoNmP0KuZPFRlp2D07oLEXFylwpfOw6v2xmwqxajGiL51qrf0fhS5CT7zFbkHDmZljPfOo6Tzw==", "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@babel/helper-plugin-utils": "7.0.0-beta.53", + "@babel/helper-replace-supers": "7.0.0-beta.53" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.0.0-beta.53.tgz", + "integrity": "sha512-X9zQrth/hIJ8EQFIF4lc+I16MmgEBoiog0izTC37wwXHEMtC9UWtspfDNVOIwuJ5vqKbQn8arqCUCh63SSG90g==", "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "@babel/helper-call-delegate": "7.0.0-beta.53", + "@babel/helper-get-function-arity": "7.0.0-beta.53", + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@babel/plugin-transform-parameters/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, "engines": { - "node": ">=14" - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0-beta.53.tgz", + "integrity": "sha512-mGWykD0r9/7isJjTMG45kgUb63zWp9Rx1Mrd0tt8928IlNx4V2/1zjB1RqObiBE+ylkmhz6G3ywNmXuXSy9haQ==", + "license": "MIT", + "dependencies": { + "regenerator-transform": "^0.13.3" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0-beta.53.tgz", + "integrity": "sha512-kNjYrpDKi+ZYjWM1qQwD10ERplviaDb37/9RoLpeIRezf+DXPm5PMdIMYJH2gD552fbtkYwESp44hm2Izpi3rg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.0.0-beta.53.tgz", + "integrity": "sha512-pkK2dpGXiblw+OojZnyMdJAD/qUs0bDnhqqmN4WLdbvsbQW0wC/nWD7YmGUwl9B8kJ4cFmrvT1l5idh4d9Az3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" + } + }, + "node_modules/@babel/plugin-transform-spread/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.0.0-beta.53.tgz", + "integrity": "sha512-axZvAsF66i0/hBqtlAeVWB66OGx+EU/5cY4DEQtqd217LdbtrTpV3oynG0cZylNPeY2TCU848ojlBCA1L3PvTQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53", + "@babel/helper-regex": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0-beta.53.tgz", + "integrity": "sha512-0q5OZuPVBAB/rsqulVLWT/bEoT1dEcKiVkyUagKgaVer2rXy1eB6eSFV3cJ/gpnlXDB2L0dCgeakgGJz/a1q4g==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "7.0.0-beta.53", + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" + } + }, + "node_modules/@babel/plugin-transform-template-literals/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.0.0-beta.53.tgz", + "integrity": "sha512-xm9X4m0x+HrZ9r8GqNFjnFlip0nh7zUjGzGFOFD1l07gepng3tG1HYrO+LJ9WwOqnE1pqc8d0cCz7nvxlEqHLQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.0.0-beta.53.tgz", + "integrity": "sha512-6DlspW3xuGi9JKzof3cqdel69TF/bE0tn7wC3tl1+VZ+BnUauEcZjlaN8azTD5YfgEUuiqWl9+Oz0WOyBf+0Yw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53", + "@babel/helper-regex": "7.0.0-beta.53", + "regexpu-core": "^4.1.3" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/preset-es2015": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/preset-es2015/-/preset-es2015-7.0.0-beta.53.tgz", + "integrity": "sha512-rcLuTFjJ4jlJdjkFeyX/BUyht3tGmfa3fgtAlPafNLLsAZ6nriJFhFNSXdB6Sl+seTcKVYvZoEFWNuVvqDXrnQ==", + "deprecated": "๐Ÿ‘‹ We've deprecated any official yearly presets in 6.x in favor or babel-preset-env. For 7.x it would be @babel/preset-env.", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "7.0.0-beta.53", + "@babel/plugin-transform-arrow-functions": "7.0.0-beta.53", + "@babel/plugin-transform-block-scoped-functions": "7.0.0-beta.53", + "@babel/plugin-transform-block-scoping": "7.0.0-beta.53", + "@babel/plugin-transform-classes": "7.0.0-beta.53", + "@babel/plugin-transform-computed-properties": "7.0.0-beta.53", + "@babel/plugin-transform-destructuring": "7.0.0-beta.53", + "@babel/plugin-transform-duplicate-keys": "7.0.0-beta.53", + "@babel/plugin-transform-for-of": "7.0.0-beta.53", + "@babel/plugin-transform-function-name": "7.0.0-beta.53", + "@babel/plugin-transform-instanceof": "7.0.0-beta.53", + "@babel/plugin-transform-literals": "7.0.0-beta.53", + "@babel/plugin-transform-modules-amd": "7.0.0-beta.53", + "@babel/plugin-transform-modules-commonjs": "7.0.0-beta.53", + "@babel/plugin-transform-modules-systemjs": "7.0.0-beta.53", + "@babel/plugin-transform-modules-umd": "7.0.0-beta.53", + "@babel/plugin-transform-object-super": "7.0.0-beta.53", + "@babel/plugin-transform-parameters": "7.0.0-beta.53", + "@babel/plugin-transform-regenerator": "7.0.0-beta.53", + "@babel/plugin-transform-shorthand-properties": "7.0.0-beta.53", + "@babel/plugin-transform-spread": "7.0.0-beta.53", + "@babel/plugin-transform-sticky-regex": "7.0.0-beta.53", + "@babel/plugin-transform-template-literals": "7.0.0-beta.53", + "@babel/plugin-transform-typeof-symbol": "7.0.0-beta.53", + "@babel/plugin-transform-unicode-regex": "7.0.0-beta.53" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.50 <7.0.0-rc.0" + } + }, + "node_modules/@babel/preset-es2015/node_modules/@babel/helper-plugin-utils": { + "version": "7.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.53.tgz", + "integrity": "sha512-ziTIzKm3Hj8LvmV6HwyPC2t2NgSNg2T72Cifqaw3zo44ATRUeNI/nH7NoQZChNNwye97pbzs+UAHq6fCTt3uFg==", + "license": "MIT" + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, "engines": { - "node": ">=14.0.0" + "node": ">=6.9.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==", + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "license": "Apache-2.0", + "dependencies": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + }, + "bin": { + "watch": "cli.js" + }, + "engines": { + "node": ">=0.1.95" + } + }, + "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" + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "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" ], @@ -1063,12 +1805,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "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" ], @@ -1077,12 +1822,32 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "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" ], @@ -1091,12 +1856,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "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" ], @@ -1105,12 +1873,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "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" ], @@ -1119,12 +1890,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "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" ], @@ -1133,12 +1907,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "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" ], @@ -1147,68 +1924,83 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "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": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "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": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "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": [ - "arm64" + "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "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": [ - "loong64" + "mips64el" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "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" ], @@ -1217,12 +2009,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "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" ], @@ -1231,40 +2026,49 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "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": [ - "riscv64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "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": [ - "s390x" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "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" ], @@ -1272,13 +2076,16 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "netbsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "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" ], @@ -1286,13 +2093,33 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "openbsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "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" ], @@ -1301,12 +2128,15 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "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" ], @@ -1315,12 +2145,15 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "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" ], @@ -1329,2810 +2162,8544 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=12" + } }, - "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==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz", + "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "jest-message-util": "^25.5.0", + "jest-util": "^25.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", + "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/@jest/fake-timers": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", + "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "lolex": "^5.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/@jest/globals": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-25.5.2.tgz", + "integrity": "sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^25.5.0", + "@jest/types": "^25.5.0", + "expect": "^25.5.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/@jest/source-map": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz", + "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/@jest/test-result": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz", + "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==", + "license": "MIT", + "dependencies": { + "@jest/console": "^25.5.0", + "@jest/types": "^25.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.5.4.tgz", + "integrity": "sha512-pTJGEkSeg1EkCO2YWq6hbFvKNXk8ejqlxiOg1jBNLnWrgXOkdY6UmqZpwGFXNnRt9B8nO1uWMzLLZ4eCmhkPNA==", + "license": "MIT", + "dependencies": { + "@jest/test-result": "^25.5.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^25.5.1", + "jest-runner": "^25.5.4", + "jest-runtime": "^25.5.4" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/@jest/transform": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz", + "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^25.5.0", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^3.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^25.5.1", + "jest-regex-util": "^25.2.6", + "jest-util": "^25.5.0", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "realpath-native": "^2.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.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.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/frappe-gantt": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/frappe-gantt/-/frappe-gantt-0.9.0.tgz", + "integrity": "sha512-n00ElvRvJ1/+HkJwt57yjnTtAM7FcH/pEV9LbRCy3+hR39TY6l0mQuy4o909uxvw97aCNhQjNh8J8xACKJ2G3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "license": "MIT" + }, + "node_modules/@types/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.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/react-vertical-timeline-component": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@types/react-vertical-timeline-component/-/react-vertical-timeline-component-3.3.6.tgz", + "integrity": "sha512-OUvyPXRjXvUD/SNLO0CW0GbIxVF32Ios5qHecMSfw6kxnK1cPULD9NV80EuqZ3WmS/s6BgbcwmN8k4ISb3akhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "15.0.20", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.20.tgz", + "integrity": "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "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/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "license": "MIT", + "dependencies": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-equal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.2.tgz", + "integrity": "sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", + "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==", + "license": "MIT", + "dependencies": { + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^25.5.0", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + }, + "engines": { + "node": ">= 8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz", + "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/babel-polyfill": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.23.0.tgz", + "integrity": "sha512-0l7mVU+LrQ2X/ZTUq63T5i3VyR2aTgcRTFmBcD6djQ/Fek6q1A9t5u0F4jZVYHzp78jwWAzGfLpAY1b4/I3lfg==", + "license": "MIT", + "dependencies": { + "babel-runtime": "^6.22.0", + "core-js": "^2.4.0", + "regenerator-runtime": "^0.10.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.4.tgz", + "integrity": "sha512-5/INNCYhUGqw7VbVjT/hb3ucjgkVHKXY7lX3ZjlN4gm565VyFmJUrJ/h+h16ECVB38R/9SF6aACydpKMLZ/c9w==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz", + "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^25.5.0", + "babel-preset-current-node-syntax": "^0.1.2" + }, + "engines": { + "node": ">= 8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "license": "MIT", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "license": "MIT", + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/batch-processor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/batch-processor/-/batch-processor-1.0.0.tgz", + "integrity": "sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA==", + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "license": "BSD-2-Clause" + }, + "node_modules/browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "license": "MIT", + "dependencies": { + "resolve": "1.1.7" + } + }, + "node_modules/browser-resolve/node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "license": "MIT", + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001734", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", + "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "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/capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "license": "ISC", + "dependencies": { + "rsvp": "^4.8.4" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha512-j/Toj7f1z98Hh2cYo2BVr85EpIRWqUi7rtRSGxh/cqUjqrnJe9l9UE7IUGd2vQ2p+kSHLkSzObQPZPLUC6TQwg==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT" + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "license": "MIT" + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "license": "MIT", + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, + "node_modules/cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", + "license": "MIT", + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "deprecated": "Use your platform's native DOMException instead", + "license": "MIT", + "dependencies": { + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.200", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz", + "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", + "license": "ISC" + }, + "node_modules/element-resize-detector": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.2.4.tgz", + "integrity": "sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==", + "license": "MIT", + "dependencies": { + "batch-processor": "1.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exec-sh": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", + "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "license": "MIT", + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/expect": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", + "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^25.5.0", + "ansi-styles": "^4.0.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-regex-util": "^25.2.6" + }, + "engines": { + "node": ">= 8.3" + } + }, + "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/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "license": "MIT", + "dependencies": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "license": "MIT", + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "license": "MIT", + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/frappe-gantt": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/frappe-gantt/-/frappe-gantt-1.0.4.tgz", + "integrity": "sha512-N94OP9ZiapaG5nzgCeZdxsKP8HD5aLVlH5sEHxSNZQnNKQ4BOn2l46HUD+KIE0LpYIterP7gIrFfkLNRuK0npQ==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "license": "ISC" + }, + "node_modules/html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^1.0.1" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.0.6.tgz", + "integrity": "sha512-thluxTGBXUGb8DuQcvH9/CM/CrcGyB5xUpWc9x6Slqcq1z/hRr2a6KxUpX4ddRfmbe0hg3E4jTvo5833aWz3BA==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^1.1.0", + "chalk": "^1.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.1", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx": "^4.1.0", + "string-width": "^2.0.0", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/string-width/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/string-width/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "license": "MIT", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-config": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz", + "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^25.5.4", + "@jest/types": "^25.5.0", + "babel-jest": "^25.5.1", + "chalk": "^3.0.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-environment-jsdom": "^25.5.0", + "jest-environment-node": "^25.5.0", + "jest-get-type": "^25.2.6", + "jest-jasmine2": "^25.5.4", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "micromatch": "^4.0.2", + "pretty-format": "^25.5.0", + "realpath-native": "^2.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "license": "MIT", + "dependencies": { + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz", + "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==", + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-each": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz", + "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "jest-get-type": "^25.2.6", + "jest-util": "^25.5.0", + "pretty-format": "^25.5.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz", + "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^25.5.0", + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "jsdom": "^15.2.1" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-environment-node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz", + "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^25.5.0", + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-environment-node/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", + "license": "MIT", + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-haste-map": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz", + "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^25.5.0", + "@types/graceful-fs": "^4.1.2", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-serializer": "^25.5.0", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 8.3" + }, + "optionalDependencies": { + "fsevents": "^2.1.2" + } + }, + "node_modules/jest-jasmine2": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz", + "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^25.5.0", + "@jest/source-map": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "co": "^4.6.0", + "expect": "^25.5.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^25.5.0", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-runtime": "^25.5.4", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "pretty-format": "^25.5.0", + "throat": "^5.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-jasmine2/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-leak-detector": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz", + "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==", + "license": "MIT", + "dependencies": { + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-matcher-utils": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", + "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", + "license": "MIT", + "dependencies": { + "chalk": "^3.0.0", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "engines": { + "node": ">= 8.3" } }, - "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, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" } }, - "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, + "node_modules/jest-message-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", + "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "@babel/code-frame": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + }, + "engines": { + "node": ">= 8.3" } }, - "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, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" + "node_modules/jest-mock": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", + "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^25.5.0" + }, + "engines": { + "node": ">= 8.3" + } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" + "node_modules/jest-regex-util": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", + "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", + "license": "MIT", + "engines": { + "node": ">= 8.3" + } }, - "node_modules/@types/react": { - "version": "18.3.23", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", - "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", - "dev": true, + "node_modules/jest-resolve": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz", + "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "@jest/types": "^25.5.0", + "browser-resolve": "^1.11.3", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.1", + "read-pkg-up": "^7.0.1", + "realpath-native": "^2.0.0", + "resolve": "^1.17.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">= 8.3" } }, - "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, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", - "dev": true, - "license": "MIT" + "node_modules/jest-runner": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz", + "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==", + "license": "MIT", + "dependencies": { + "@jest/console": "^25.5.0", + "@jest/environment": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-config": "^25.5.4", + "jest-docblock": "^25.3.0", + "jest-haste-map": "^25.5.1", + "jest-jasmine2": "^25.5.4", + "jest-leak-detector": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-resolve": "^25.5.1", + "jest-runtime": "^25.5.4", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" + }, + "engines": { + "node": ">= 8.3" + } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, + "node_modules/jest-runner/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz", + "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==", + "license": "MIT", + "dependencies": { + "@jest/console": "^25.5.0", + "@jest/environment": "^25.5.0", + "@jest/globals": "^25.5.2", + "@jest/source-map": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-config": "^25.5.4", + "jest-haste-map": "^25.5.1", + "jest-message-util": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "realpath-native": "^2.0.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^15.3.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "bin": { + "jest-runtime": "bin/jest-runtime.js" }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/jest-serializer": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz", + "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" + "graceful-fs": "^4.2.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": ">= 8.3" + } + }, + "node_modules/jest-snapshot": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz", + "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/prettier": "^1.19.0", + "chalk": "^3.0.0", + "expect": "^25.5.0", + "graceful-fs": "^4.2.4", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-resolve": "^25.5.1", + "make-dir": "^3.0.0", + "natural-compare": "^1.4.0", + "pretty-format": "^25.5.0", + "semver": "^6.3.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/jest-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", + "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "make-dir": "^3.0.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">= 8.3" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, + "node_modules/jest-util/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz", + "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^25.5.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "jest-get-type": "^25.2.6", + "leven": "^3.1.0", + "pretty-format": "^25.5.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", + "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", + "license": "MIT", + "dependencies": { + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "argparse": "^2.0.1" }, - "engines": { - "node": "^16.0.0 || >=18.0.0" + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.0", + "acorn": "^7.1.0", + "acorn-globals": "^4.3.2", + "array-equal": "^1.0.0", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.1", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.2.0", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", + "xml-name-validator": "^3.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "engines": { + "node": ">=8" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "canvas": "^2.5.0" }, "peerDependenciesMeta": { - "typescript": { + "canvas": { "optional": true } } }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, + "node_modules/jsdom/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": ">=0.4.0" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "engines": { + "node": ">=6" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=6" } }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "node": ">=0.6.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=0.10.0" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "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==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { - "@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" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "node": ">= 0.8.0" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, + "node_modules/lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "license": "BSD-3-Clause", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@sinonjs/commons": "^1.7.0" + } + }, + "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" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node_modules/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==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "node_modules/lucide-react": { + "version": "0.294.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.294.0.tgz", + "integrity": "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "semver": "^6.0.0" }, "engines": { "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" + "object-visit": "^1.0.0" }, "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=0.10.0" } }, - "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 8" } }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "mime-db": "1.52.0" }, "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/browserslist": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", - "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", - "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" - } - ], + "node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", "dependencies": { - "caniuse-lite": "^1.0.30001733", - "electron-to-chromium": "^1.5.199", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "brace-expansion": "^2.0.1" }, - "bin": { - "browserslist": "cli.js" + "engines": { + "node": ">=16 || 14 >=14.17" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==", + "license": "MIT" + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", "engines": { - "node": ">=6" + "node": "*" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", + "license": "ISC" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 6" + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001734", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", - "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=0.10.0" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz", + "integrity": "sha512-BDxbhLHXFFFvilHjh9xihcDyPkXQ+kjblxnl82zAX41xUYSNvuRpFRznmldR9+OKu+p+ULZ7hNoyunlLB5ecUA==", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "encoding": "^0.1.11", + "is-stream": "^1.0.1" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">=0.10.0" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "path-key": "^2.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=4" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=4" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, + "node_modules/nwsapi": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", "engines": { - "node": ">= 8" + "node": "*" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "license": "MIT", "dependencies": { - "path-type": "^4.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/doctrine": { + "node_modules/object-hash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "license": "MIT", "dependencies": { - "esutils": "^2.0.2" + "isobject": "^3.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=0.10.0" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "isobject": "^3.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } }, - "node_modules/electron-to-chromium": { - "version": "1.5.200", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz", - "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", - "dev": true, - "license": "ISC" + "node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" + "node_modules/opencollective": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/opencollective/-/opencollective-1.0.3.tgz", + "integrity": "sha512-YBRI0Qa8+Ui0/STV1qYuPrJm889PT3oCPHMVoL+8Y3nwCffj7PSrB2NlGgrhgBKDujxTjxknHWJ/FiqOsYcIDw==", + "license": "MIT", + "dependencies": { + "babel-polyfill": "6.23.0", + "chalk": "1.1.3", + "inquirer": "3.0.6", + "minimist": "1.2.0", + "node-fetch": "1.6.3", + "opn": "4.0.2" + }, + "bin": { + "oc": "dist/bin/opencollective.js", + "opencollective": "dist/bin/opencollective.js" + } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, + "node_modules/opencollective/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/opencollective/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/opencollective/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/opencollective/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=0.8.0" } }, - "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, + "node_modules/opencollective/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "ansi-regex": "^2.0.0" }, "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": ">=0.10.0" } }, - "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, + "node_modules/opencollective/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.8.0" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, + "node_modules/opn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz", + "integrity": "sha512-iPBWbPP4OEOzR1xfhpGLDh+ypKBOygunZhM9jBtA7FS5sKjEiMZw0EFb82hnDOmTZX90ZWLoZKUza4cVt8MexA==", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 0.8.0" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, "engines": { "node": ">=10" }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "p-limit": "^3.0.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=6" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "license": "BlueOak-1.0.0" }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "callsites": "^3.0.0" }, "engines": { - "node": "*" + "node": ">=6" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } + "node_modules/parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "license": "MIT" }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=0.10.0" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=8" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, "engines": { - "node": ">= 6" + "node": ">=8" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "flat-cache": "^3.0.4" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, "engines": { "node": ">=8" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=0.10.0" } }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "license": "MIT", "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "pinkie": "^2.0.0" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=0.10.0" } }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, "engines": { "node": ">= 6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, + "node_modules/pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "license": "MIT" + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", "license": "MIT", "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" + "node": ">=0.10.0" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" + "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" + } ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "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==", + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "camelcase-css": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": "^12 || ^14 || >= 16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" }, "engines": { - "node": ">= 0.4" + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "dev": true, - "license": "ISC", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "postcss-selector-parser": "^6.1.1" }, "engines": { - "node": "*" + "node": ">=12.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "postcss": "^8.2.14" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=10.13.0" + "node": ">=4" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", + "node_modules/pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" }, "engines": { - "node": "*" + "node": ">= 8.3" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, + "node_modules/private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "punycode": "^2.3.1" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/lupomontero" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.6" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "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": { - "has-symbols": "^1.0.3" + "loose-envify": "^1.1.0" }, "engines": { - "node": ">= 0.4" + "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" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "react": "^18.3.1" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/react-gantt-timeline": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/react-gantt-timeline/-/react-gantt-timeline-0.4.3.tgz", + "integrity": "sha512-2XpWVjmczo0ooSomyYT6VXFWsX9iOQsBPa+NwfBqMYNYsT9XJ5nbD2zPEEVQx/r2icJqv9k0lVQdWx76GOcnCg==", + "hasInstallScript": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "jest-config": "^25.1.0", + "moment": "^2.24.0", + "opencollective": "^1.0.3", + "opencollective-postinstall": "^2.0.1", + "react-sizeme": "^2.5.2" }, - "engines": { - "node": ">= 0.4" + "peerDependencies": { + "css-layout": "^1.1.1", + "moment": "^2.22.1", + "react": "^16.3.2", + "react-dom": "^16.3.2", + "react-sizeme": "^2.5.2" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/react-intersection-observer": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-8.34.0.tgz", + "integrity": "sha512-TYKh52Zc0Uptp5/b4N91XydfSGKubEhgZRtcg1rhTKABXijc4Sdr1uTp5lJ8TN27jwUsdXxjHXtHa0kPj704sw==", + "license": "MIT", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0|| ^18.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": ">=0.10.0" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@remix-run/router": "1.23.0" }, "engines": { - "node": ">=6" + "node": ">=14.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "react": ">=16.8" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, "engines": { - "node": ">=0.8.19" + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", + "node_modules/react-sizeme": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/react-sizeme/-/react-sizeme-2.6.12.tgz", + "integrity": "sha512-tL4sCgfmvapYRZ1FO2VmBmjPVzzqgHA7kI8lSJ6JS6L78jXFNRdOZFpXyK6P1NBZvKPPCZxReNgzZNUajAerZw==", + "license": "MIT", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "element-resize-detector": "^1.2.1", + "invariant": "^2.2.4", + "shallowequal": "^1.1.0", + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0", + "react-dom": "^0.14.0 || ^15.0.0-0 || ^16.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/react-vertical-timeline-component": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/react-vertical-timeline-component/-/react-vertical-timeline-component-3.5.3.tgz", + "integrity": "sha512-GS3kcppKpxbgOJDbPWsO3B6/MktVuo/YzebrOHcf5IF9VfOGeKysJuMyuKkvp6XDWAm0n1wlIYyYK1GxiGXSqQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@babel/preset-es2015": "^7.0.0-beta.53", + "classnames": "^2.2.6", + "react-intersection-observer": "^8.26.2" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" }, "engines": { "node": ">=8" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, "engines": { "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "p-try": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, "engines": { - "node": ">=0.12.0" + "node": ">=8" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "picomatch": "^2.2.1" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "engines": { + "node": ">=8.10.0" } }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "node_modules/realpath-native": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", + "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "node_modules/regenerate-unicode-properties": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz", + "integrity": "sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==", "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "regenerate": "^1.4.2" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=4" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, + "node_modules/regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==", + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.4.tgz", + "integrity": "sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A==", "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" + "dependencies": { + "private": "^0.1.6" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" + "node_modules/regexpu-core": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz", + "integrity": "sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^9.0.0", + "regjsgen": "^0.5.2", + "regjsparser": "^0.7.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "node_modules/regjsgen": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", "license": "MIT" }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" + "node_modules/regjsparser": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz", + "integrity": "sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } }, - "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", + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" + "jsesc": "bin/jsesc" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "license": "ISC" + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 6" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.19" + }, "engines": { - "node": ">=14" + "node": ">=0.10.0" }, - "funding": { - "url": "https://github.com/sponsors/antonk52" + "peerDependencies": { + "request": "^2.34" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", + "node_modules/request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "deprecated": "request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "license": "ISC", "dependencies": { - "p-locate": "^5.0.0" + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" }, "engines": { - "node": ">=10" + "node": ">=0.12.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "request": "^2.34" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/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==", + "node_modules/request-promise-native/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "license": "MIT", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" }, - "bin": { - "loose-envify": "cli.js" + "engines": { + "node": ">= 0.12" } }, - "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", + "node_modules/request/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", "dependencies": { - "yallist": "^3.0.2" + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" } }, - "node_modules/lucide-react": { - "version": "0.294.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.294.0.tgz", - "integrity": "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=4" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "license": "MIT" + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": ">=8.6" + "node": ">=4" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=0.12" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, "engines": { - "node": ">= 0.6" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "glob": "^7.1.3" }, - "engines": { - "node": ">=16 || 14 >=14.17" + "bin": { + "rimraf": "bin.js" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" + "node_modules/rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "license": "MIT", + "engines": { + "node": "6.* || >= 7.*" + } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "engines": { + "node": ">=0.12.0" } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { "type": "github", - "url": "https://github.com/sponsors/ai" + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "dependencies": { + "queue-microtask": "^1.2.2" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", + "license": "Apache-2.0" }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "ret": "~0.1.10" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "deprecated": "some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added", "license": "MIT", + "dependencies": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "bin": { + "sane": "src/cli.js" + }, "engines": { - "node": ">= 6" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "node_modules/sane/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "license": "ISC", "dependencies": { - "wrappy": "1" + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, + "node_modules/sane/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "node_modules/sane/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, + "node_modules/sane/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, + "node_modules/sane/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "license": "MIT", "dependencies": { - "callsites": "^3.0.0" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "node_modules/sane/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "node_modules/sane/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" + "kind-of": "^3.0.2" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "node_modules/sane/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "license": "MIT", - "engines": { - "node": ">=8.6" + "dependencies": { + "is-buffer": "^1.1.5" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, + "node_modules/sane/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "license": "MIT", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, "engines": { - "node": ">= 6" + "node": ">=0.10.0" } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/sane/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "remove-trailing-separator": "^1.0.1" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=0.10.0" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, + "node_modules/sane/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" + "node": ">=0.10.0" } }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "license": "MIT", + "node_modules/saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "license": "ISC", "dependencies": { - "camelcase-css": "^2.0.1" + "xmlchars": "^2.1.1" }, "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" + "node": ">=8" } }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "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": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } + "node": ">=10" } }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.1.1" + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" }, "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" + "node": ">=0.10.0" } }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "license": "MIT", "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/proxy-from-env": { + "node_modules/shallowequal": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "license": "MIT" }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "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==", + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0" + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.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==", + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" }, - "peerDependencies": { - "react": "^18.3.1" + "engines": { + "node": ">=0.10.0" } }, - "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, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "kind-of": "^3.2.0" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" + "node": ">=0.10.0" } }, - "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "is-buffer": "^1.1.5" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "node": ">=0.10.0" } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { - "pify": "^2.3.0" + "ms": "2.0.0" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "is-descriptor": "^0.1.0" }, "engines": { - "node": ">=8.10.0" + "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "is-extendable": "^0.1.0" }, - "bin": { - "resolve": "bin/resolve" + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", "engines": { - "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", - "dev": true, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", - "fsevents": "~2.3.2" + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", "dependencies": { - "queue-microtask": "^1.2.2" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "license": "MIT" + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "license": "CC0-1.0" + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, "bin": { - "semver": "bin/semver.js" + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "node_modules/stack-utils": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz", + "integrity": "sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==", "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "escape-string-regexp": "^2.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, "engines": { - "node": ">=14" + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "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", + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "license": "ISC", "engines": { "node": ">=0.10.0" } @@ -4211,7 +10778,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -4234,6 +10800,24 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4311,7 +10895,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -4324,7 +10907,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4333,6 +10915,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", @@ -4371,6 +10959,42 @@ "node": ">=14.0.0" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4401,17 +11025,135 @@ "node": ">=0.8" } }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "license": "MIT" + }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "license": "MIT", + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, "engines": { - "node": ">=8.0" + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "license": "BSD-3-Clause", + "dependencies": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, "node_modules/ts-api-utils": { @@ -4434,6 +11176,24 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4447,6 +11207,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -4460,6 +11229,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", @@ -4474,11 +11252,128 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4509,12 +11404,27 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "license": "MIT" + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4522,6 +11432,40 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/vite": { "version": "5.4.19", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", @@ -4582,11 +11526,72 @@ } } }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", + "license": "MIT", + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "license": "MIT", + "dependencies": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -4598,11 +11603,16 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4713,14 +11723,91 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "license": "Apache-2.0" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "license": "ISC" }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -4736,6 +11823,113 @@ "node": ">= 14.6" } }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/backends/advanced/webui/package.json b/backends/advanced/webui/package.json index 17894a86..f00a8096 100644 --- a/backends/advanced/webui/package.json +++ b/backends/advanced/webui/package.json @@ -10,16 +10,25 @@ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" }, "dependencies": { + "@types/d3": "^7.4.3", "axios": "^1.6.2", "clsx": "^2.0.0", + "d3": "^7.9.0", + "frappe-gantt": "^1.0.4", "lucide-react": "^0.294.0", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.20.0" + "react-gantt-timeline": "^0.4.3", + "react-router-dom": "^6.20.0", + "react-vertical-timeline-component": "^3.5.3", + "xss": "^1.0.15" }, "devDependencies": { + "@types/frappe-gantt": "^0.9.0", "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", + "@types/react-vertical-timeline-component": "^3.3.6", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", @@ -32,4 +41,4 @@ "typescript": "^5.2.2", "vite": "^5.0.8" } -} \ No newline at end of file +} diff --git a/backends/advanced/webui/vite.config.ts b/backends/advanced/webui/vite.config.ts index a3b411c3..8bad98b0 100644 --- a/backends/advanced/webui/vite.config.ts +++ b/backends/advanced/webui/vite.config.ts @@ -22,5 +22,12 @@ export default defineConfig({ build: { outDir: 'dist', sourcemap: true, + rollupOptions: { + external: [], + }, + commonjsOptions: { + include: [/node_modules/], + transformMixedEsModules: true, + }, }, }) \ No newline at end of file diff --git a/compose/advanced-backend.yml b/compose/advanced-backend.yml new file mode 100644 index 00000000..6ab98543 --- /dev/null +++ b/compose/advanced-backend.yml @@ -0,0 +1,7 @@ +# Friend-Lite Advanced Backend +# Main backend services - API, workers, databases, web UI +# This is the core Friend-Lite stack + +include: + - path: ../backends/advanced/docker-compose.yml + env_file: ../backends/advanced/.env diff --git a/compose/asr-services.yml b/compose/asr-services.yml new file mode 100644 index 00000000..807be71b --- /dev/null +++ b/compose/asr-services.yml @@ -0,0 +1,28 @@ +# ASR (Automatic Speech Recognition) Services +# Offline transcription with Parakeet +# Enable with: docker compose --profile asr up + +services: + parakeet-asr: + build: + context: ../extras/asr-services + dockerfile: Dockerfile.parakeet + ports: + - "${PARAKEET_PORT:-8767}:8767" + environment: + - MODEL_NAME=${PARAKEET_MODEL:-parakeet-ctc-1.1b} + profiles: + - asr + restart: unless-stopped + networks: + - chronicle-network + deploy: + resources: + limits: + cpus: '4' + memory: 8G + +networks: + chronicle-network: + name: chronicle-network + external: true diff --git a/compose/mycelia.yml b/compose/mycelia.yml new file mode 100644 index 00000000..d8ed9384 --- /dev/null +++ b/compose/mycelia.yml @@ -0,0 +1,69 @@ +# Mycelia Services +# AI memory and timeline service +# Enable with: docker compose --profile mycelia up + +services: + mycelia-backend: + build: + context: ../extras/mycelia/backend + dockerfile: Dockerfile.simple + command: ["deno", "run", "-A", "--env", "server.ts", "serve", "-p", "5100"] + ports: + - "${MYCELIA_BACKEND_PORT:-5100}:5100" + environment: + # Shared JWT secret for Friend-Lite authentication + - JWT_SECRET=${AUTH_SECRET_KEY} + - SECRET_KEY=${AUTH_SECRET_KEY} + # MongoDB connection (Docker internal) + - MONGO_URL=mongodb://mongo:27017 + - MONGO_DB=${MYCELIA_DB:-mycelia} + - DATABASE_NAME=${MYCELIA_DB:-mycelia} + # Redis connection (ioredis uses host/port, not URL) + - REDIS_HOST=redis + - REDIS_PORT=6379 + volumes: + - ../extras/mycelia/backend/app:/app/app + depends_on: + mongo: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "deno", "eval", "fetch('http://localhost:5100/health').then(r => r.ok ? Deno.exit(0) : Deno.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + restart: unless-stopped + profiles: + - mycelia + networks: + - mem0-network + - chronicle-network + + mycelia-frontend: + build: + context: ../extras/mycelia + dockerfile: frontend/Dockerfile.simple + ports: + - "${MYCELIA_FRONTEND_PORT:-3003}:8080" + environment: + - VITE_API_URL=${MYCELIA_API_URL:-http://localhost:5100} + volumes: + - ../extras/mycelia/frontend/src:/app/src + depends_on: + mycelia-backend: + condition: service_healthy + restart: unless-stopped + profiles: + - mycelia + networks: + - mem0-network + - chronicle-network + +networks: + mem0-network: + driver: bridge + chronicle-network: + name: chronicle-network + external: true diff --git a/compose/observability.yml b/compose/observability.yml new file mode 100644 index 00000000..219ca230 --- /dev/null +++ b/compose/observability.yml @@ -0,0 +1,50 @@ +# Observability Stack +# Langfuse for LLM tracing and monitoring +# Enable with: docker compose --profile observability up + +services: + langfuse-server: + image: langfuse/langfuse:latest + ports: + - "${LANGFUSE_PORT:-3000}:3000" + environment: + - DATABASE_URL=${LANGFUSE_DATABASE_URL:-postgresql://postgres:postgres@langfuse-db:5432/langfuse} + - NEXTAUTH_SECRET=${LANGFUSE_NEXTAUTH_SECRET:-changeme} + - NEXTAUTH_URL=${LANGFUSE_URL:-http://localhost:3000} + - SALT=${LANGFUSE_SALT:-changeme} + depends_on: + langfuse-db: + condition: service_healthy + profiles: + - observability + restart: unless-stopped + networks: + - chronicle-network + + langfuse-db: + image: postgres:15-alpine + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=langfuse + volumes: + - langfuse_db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + profiles: + - observability + restart: unless-stopped + networks: + - chronicle-network + +networks: + chronicle-network: + name: chronicle-network + external: true + +volumes: + langfuse_db: + driver: local diff --git a/compose/openmemory.yml b/compose/openmemory.yml new file mode 100644 index 00000000..c84ff9c6 --- /dev/null +++ b/compose/openmemory.yml @@ -0,0 +1,50 @@ +# OpenMemory MCP Server +# Cross-client memory compatibility +# Enable with: docker compose --profile openmemory up + +services: + openmemory-mcp: + build: + context: ../extras/openmemory-mcp + dockerfile: Dockerfile + ports: + - "${OPENMEMORY_PORT:-8765}:8765" + environment: + - OPENAI_API_KEY=${OPENAI_API_KEY} + - OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory} + - QDRANT_URL=${QDRANT_URL:-http://qdrant:6334} + depends_on: + - qdrant + profiles: + - openmemory + restart: unless-stopped + networks: + - chronicle-network + healthcheck: + test: ["CMD", "python", "-c", "import requests; exit(0 if requests.get('http://localhost:8765/docs').status_code == 200 else 1)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + # OpenMemory UI (optional within openmemory profile) + openmemory-ui: + image: mem0/openmemory-ui:latest + ports: + - "${OPENMEMORY_UI_PORT:-3001}:3000" + environment: + - NEXT_PUBLIC_API_URL=http://localhost:${OPENMEMORY_PORT:-8765} + - NEXT_PUBLIC_USER_ID=${OPENMEMORY_USER_ID:-openmemory} + depends_on: + - openmemory-mcp + profiles: + - openmemory + - openmemory-ui + restart: unless-stopped + networks: + - chronicle-network + +networks: + chronicle-network: + name: chronicle-network + external: true diff --git a/compose/speaker-recognition.yml b/compose/speaker-recognition.yml new file mode 100644 index 00000000..c101d14e --- /dev/null +++ b/compose/speaker-recognition.yml @@ -0,0 +1,34 @@ +# Speaker Recognition Service +# Voice identification and diarization +# Enable with: docker compose --profile speaker up + +services: + speaker-recognition: + build: + context: ../extras/speaker-recognition + dockerfile: Dockerfile + ports: + - "${SPEAKER_SERVICE_PORT:-8085}:8085" + environment: + - MODEL_PATH=/models + volumes: + - speaker_models:/models + profiles: + - speaker + restart: unless-stopped + networks: + - chronicle-network + deploy: + resources: + limits: + cpus: '2' + memory: 4G + +networks: + chronicle-network: + name: chronicle-network + external: true + +volumes: + speaker_models: + driver: local diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..3bd2e7f7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +# Friend-Lite Root Docker Compose +# Unified entry point for all Friend-Lite services +# +# Usage: +# Basic: docker compose up +# With Mycelia: docker compose --profile mycelia up +# With ASR: docker compose --profile asr up +# With Speaker ID: docker compose --profile speaker up +# With OpenMemory: docker compose --profile openmemory up +# With Observability: docker compose --profile observability up +# All services: docker compose --profile all up +# +# Multiple profiles: docker compose --profile mycelia --profile speaker up +# +# Environment-specific: +# Development: docker compose up (default) +# Testing: docker compose -f docker-compose.yml -f backends/advanced/compose/overrides/test.yml up +# Production: docker compose -f docker-compose.yml -f backends/advanced/compose/overrides/prod.yml up + +include: + # Core backend (always included) + # This includes: mongo, redis, qdrant, friend-backend, workers, webui + - compose/advanced-backend.yml + + # Optional services (profile-based) + - compose/mycelia.yml # --profile mycelia + - compose/asr-services.yml # --profile asr + - compose/speaker-recognition.yml # --profile speaker + - compose/openmemory.yml # --profile openmemory + - compose/observability.yml # --profile observability + +# Shared network for all services +# Must be created before first run: docker network create chronicle-network +networks: + chronicle-network: + name: chronicle-network + external: true + +# Note: Individual service volumes are defined in their respective compose files diff --git a/extras/openmemory-mcp/docker-compose.yml b/extras/openmemory-mcp/docker-compose.yml index 4107cf4a..383d96f0 100644 --- a/extras/openmemory-mcp/docker-compose.yml +++ b/extras/openmemory-mcp/docker-compose.yml @@ -14,10 +14,10 @@ services: context: ./cache/mem0/openmemory/api dockerfile: Dockerfile env_file: - - .env + - .env.openmemory environment: - OPENAI_API_KEY=${OPENAI_API_KEY} - - OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory} + - OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-stu} depends_on: - mem0_store ports: @@ -41,4 +41,27 @@ services: depends_on: - openmemory-mcp profiles: - - ui # Only starts when --profile ui is used \ No newline at end of file + - ui # Only starts when --profile ui is used + + # neo4j-mem0: + # image: neo4j:5.15-community + # ports: + # - "7474:7474" # HTTP + # - "7687:7687" # Bolt + # environment: + # - NEO4J_AUTH=neo4j/${NEO4J_PASSWORD:-password} + # - NEO4J_PLUGINS=["apoc"] + # - NEO4J_dbms_security_procedures_unrestricted=apoc.* + # - NEO4J_dbms_security_procedures_allowlist=apoc.* + # volumes: + # - ./data/neo4j_data:/data + # - ./data/neo4j_logs:/logs + # restart: unless-stopped + + + + +networks: + default: + name: chronicle-network + external: true \ No newline at end of file diff --git a/extras/speaker-recognition/run-test.sh b/extras/speaker-recognition/run-test.sh index 6ac212fa..cf82eb22 100755 --- a/extras/speaker-recognition/run-test.sh +++ b/extras/speaker-recognition/run-test.sh @@ -69,8 +69,8 @@ elif [ -n "${HF_TOKEN:-}" ]; then export SIMILARITY_THRESHOLD=0.15 export SPEAKER_SERVICE_HOST=speaker-service export COMPUTE_MODE=cpu - export SPEAKER_SERVICE_PORT=8085 - export SPEAKER_SERVICE_URL=http://speaker-service:8085 + export SPEAKER_SERVICE_PORT=8086 + export SPEAKER_SERVICE_URL=http://speaker-service:8086 export SPEAKER_SERVICE_TEST_PORT=8086 export REACT_UI_HOST=0.0.0.0 export REACT_UI_PORT=5173 diff --git a/scripts/lib/env_utils.py b/scripts/lib/env_utils.py index 8abe13a3..1f4b4b26 100644 --- a/scripts/lib/env_utils.py +++ b/scripts/lib/env_utils.py @@ -8,10 +8,11 @@ from typing import Dict, Set def get_config_env_variables() -> Set[str]: - """Get list of variable names defined in config.env""" - config_env_path = Path(__file__).parent.parent.parent / "config.env" + """Get list of variable names defined in K8s config""" + # For Kubernetes, use config-k8s.env + config_env_path = Path(__file__).parent.parent.parent / "config-k8s.env" variables = set() - + if config_env_path.exists(): with open(config_env_path, 'r') as f: for line in f: @@ -19,7 +20,17 @@ def get_config_env_variables() -> Set[str]: if line and not line.startswith('#') and '=' in line: var_name = line.split('=')[0].strip() variables.add(var_name) - + else: + # Fallback to config.env for backwards compatibility + config_env_path = Path(__file__).parent.parent.parent / "config.env" + if config_env_path.exists(): + with open(config_env_path, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + var_name = line.split('=')[0].strip() + variables.add(var_name) + return variables def get_resolved_env_vars() -> Dict[str, str]: diff --git a/start-env.sh b/start-env.sh new file mode 100755 index 00000000..deaaf6cc --- /dev/null +++ b/start-env.sh @@ -0,0 +1,260 @@ +#!/bin/bash +# Friend-Lite Environment Starter +# Loads environment-specific config and starts services +# +# Usage: +# ./start-env.sh dev # Start dev environment +# ./start-env.sh feature-123 # Start feature branch +# ./start-env.sh test # Start test environment +# ./start-env.sh dev --profile mycelia # With additional profiles + +set -e + +# Check if environment name provided +if [ -z "$1" ]; then + echo "Usage: $0 [docker-compose-options]" + echo "" + echo "Available environments:" + ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$| |' | sed 's/^/ - /' + echo "" + echo "Examples:" + echo " $0 dev # Start dev environment" + echo " $0 feature-123 # Start feature branch" + echo " $0 test # Start test environment" + echo " $0 dev --profile mycelia # Dev with mycelia" + exit 1 +fi + +ENV_NAME="$1" +shift # Remove environment name from args + +ENV_FILE="environments/${ENV_NAME}.env" + +# Check if environment file exists +if [ ! -f "$ENV_FILE" ]; then + echo "โŒ Environment file not found: $ENV_FILE" + echo "" + echo "Available environments:" + ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$| |' | sed 's/^/ - /' + exit 1 +fi + +# Load Docker Compose system defaults first +if [ -f "docker-defaults.env" ]; then + source docker-defaults.env +fi + +# Load user settings (overrides defaults) +if [ -f "config-docker.env" ]; then + source config-docker.env +elif [ -f "config.env" ]; then + # Fallback to config.env for backwards compatibility + source config.env +fi + +# Load secrets (gitignored) +if [ -f ".env.secrets" ]; then + source .env.secrets +else + echo "โš ๏ธ Warning: .env.secrets not found" + echo " Copy .env.secrets.template to .env.secrets and fill in your credentials" + echo "" +fi + +# Load environment-specific config (overrides base) +source "$ENV_FILE" + +# Calculate actual ports based on offset +BACKEND_PORT=$((8000 + PORT_OFFSET)) +WEBUI_PORT=$((3010 + PORT_OFFSET)) +MONGO_PORT=$((27017 + PORT_OFFSET)) +REDIS_PORT=$((6379 + PORT_OFFSET)) +QDRANT_GRPC_PORT=$((6033 + PORT_OFFSET)) +QDRANT_HTTP_PORT=$((6034 + PORT_OFFSET)) +MYCELIA_BACKEND_PORT=$((5100 + PORT_OFFSET)) +MYCELIA_FRONTEND_PORT=$((3003 + PORT_OFFSET)) +SPEAKER_PORT=$((8085 + PORT_OFFSET)) +OPENMEMORY_PORT=$((8765 + PORT_OFFSET)) +PARAKEET_PORT=$((8767 + PORT_OFFSET)) + +# Export all variables for docker compose +export ENV_NAME +export BACKEND_PORT +export WEBUI_PORT +export MONGO_PORT +export REDIS_PORT +export QDRANT_GRPC_PORT +export QDRANT_HTTP_PORT +export MYCELIA_BACKEND_PORT +export MYCELIA_FRONTEND_PORT +export SPEAKER_PORT +export OPENMEMORY_PORT +export PARAKEET_PORT +export MONGODB_DATABASE +export MYCELIA_DB +export QDRANT_DATA_PATH="${DATA_DIR}/qdrant_data" +export REDIS_DATA_PATH="${DATA_DIR}/redis_data" +export COMPOSE_PROJECT_NAME + +# Display configuration +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐Ÿš€ Starting Friend-Lite: ${ENV_NAME}" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "๐Ÿ“ฆ Project: ${COMPOSE_PROJECT_NAME}" +echo "๐Ÿ—„๏ธ MongoDB Database: ${MONGODB_DATABASE}" +echo "๐Ÿ—„๏ธ Mycelia Database: ${MYCELIA_DB}" +echo "๐Ÿ’พ Data Directory: ${DATA_DIR}" +echo "" +echo "๐ŸŒ Service URLs:" +echo " Backend: http://localhost:${BACKEND_PORT}" +echo " Web UI: http://localhost:${WEBUI_PORT}" +echo " MongoDB: mongodb://localhost:${MONGO_PORT}" +echo " Redis: redis://localhost:${REDIS_PORT}" +echo " Qdrant HTTP: http://localhost:${QDRANT_HTTP_PORT}" +echo " Qdrant gRPC: http://localhost:${QDRANT_GRPC_PORT}" +echo "" + +# Show optional service URLs if enabled via --profile or SERVICES variable +if [[ "$SERVICES" == *"mycelia"* ]] || [[ "$*" == *"mycelia"* ]]; then + echo "๐Ÿ“Š Mycelia Services:" + echo " Backend: http://localhost:${MYCELIA_BACKEND_PORT}" + echo " Frontend: http://localhost:${MYCELIA_FRONTEND_PORT}" + echo "" +fi + +if [[ "$SERVICES" == *"speaker"* ]] || [[ "$*" == *"speaker"* ]]; then + echo "๐ŸŽค Speaker Recognition:" + echo " Service: http://localhost:${SPEAKER_PORT}" + echo "" +fi + +if [[ "$SERVICES" == *"openmemory"* ]] || [[ "$*" == *"openmemory"* ]]; then + echo "๐Ÿง  OpenMemory MCP:" + echo " Service: http://localhost:${OPENMEMORY_PORT}" + echo "" +fi + +if [[ "$SERVICES" == *"asr"* ]] || [[ "$*" == *"parakeet"* ]]; then + echo "๐Ÿ—ฃ๏ธ Parakeet ASR:" + echo " Service: http://localhost:${PARAKEET_PORT}" + echo "" +fi +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" + +# Create data directory +mkdir -p "${DATA_DIR}" + +# Update backends/advanced/.env with environment-specific values +# This ensures the backend uses the correct database names +if [ -f "backends/advanced/.env" ]; then + # Create backup + cp backends/advanced/.env backends/advanced/.env.backup.$(date +%Y%m%d_%H%M%S) +fi + +# Generate environment-specific .env file for backend +# This exports all currently loaded environment variables to the backend +{ + echo "# Auto-generated for environment: ${ENV_NAME}" + echo "# Generated: $(date)" + echo "#" + echo "# This file is regenerated every time you run start-env.sh" + echo "# It combines: docker-defaults.env + config-docker.env + .env.secrets + environments/${ENV_NAME}.env" + echo "" + echo "# Database configuration (environment-specific)" + echo "MONGODB_URI=mongodb://mongo:27017/${MONGODB_DATABASE}" + echo "MYCELIA_DB=${MYCELIA_DB}" + echo "" + echo "# All loaded environment variables" + + # Export configuration variables from our config files + # This uses an allowlist approach to only include relevant variables + # and exclude host system paths (HOME, CONDA_*, BUNDLED_*, etc.) + + # Collect all variable names from config files + config_vars=() + + # From docker-defaults.env + if [ -f "docker-defaults.env" ]; then + while IFS='=' read -r key value; do + # Skip comments and empty lines + [[ $key =~ ^#.*$ ]] && continue + [[ -z $key ]] && continue + config_vars+=("$key") + done < <(grep -E '^[A-Z_][A-Z0-9_]*=' docker-defaults.env || true) + fi + + # From config-docker.env + if [ -f "config-docker.env" ]; then + while IFS='=' read -r key value; do + [[ $key =~ ^#.*$ ]] && continue + [[ -z $key ]] && continue + config_vars+=("$key") + done < <(grep -E '^[A-Z_][A-Z0-9_]*=' config-docker.env || true) + elif [ -f "config.env" ]; then + while IFS='=' read -r key value; do + [[ $key =~ ^#.*$ ]] && continue + [[ -z $key ]] && continue + config_vars+=("$key") + done < <(grep -E '^[A-Z_][A-Z0-9_]*=' config.env || true) + fi + + # From .env.secrets + if [ -f ".env.secrets" ]; then + while IFS='=' read -r key value; do + [[ $key =~ ^#.*$ ]] && continue + [[ -z $key ]] && continue + config_vars+=("$key") + done < <(grep -E '^[A-Z_][A-Z0-9_]*=' .env.secrets || true) + fi + + # From environment-specific file + if [ -f "$ENV_FILE" ]; then + while IFS='=' read -r key value; do + [[ $key =~ ^#.*$ ]] && continue + [[ -z $key ]] && continue + config_vars+=("$key") + done < <(grep -E '^[A-Z_][A-Z0-9_]*=' "$ENV_FILE" || true) + fi + + # Add port variables calculated by this script + config_vars+=( + "BACKEND_PORT" + "WEBUI_PORT" + "MONGO_PORT" + "REDIS_PORT" + "QDRANT_GRPC_PORT" + "QDRANT_HTTP_PORT" + "MYCELIA_BACKEND_PORT" + "MYCELIA_FRONTEND_PORT" + "SPEAKER_PORT" + "OPENMEMORY_PORT" + "PARAKEET_PORT" + "QDRANT_DATA_PATH" + "REDIS_DATA_PATH" + ) + + # Remove duplicates and sort + config_vars=($(printf '%s\n' "${config_vars[@]}" | sort -u)) + + # Export only the allowlisted variables + for key in "${config_vars[@]}"; do + if [ -n "${!key}" ]; then + echo "${key}=${!key}" + fi + done +} > backends/advanced/.env.${ENV_NAME} + +# Symlink to active .env +ln -sf .env.${ENV_NAME} backends/advanced/.env + +echo "โœ… Environment configured" +echo "๐Ÿ“ Backend .env: backends/advanced/.env.${ENV_NAME}" +echo "" + +# Start services +echo "๐Ÿš€ Starting Docker Compose..." +echo "" + +docker compose "$@" up diff --git a/tests/setup/test_env.py b/tests/setup/test_env.py index c250262b..e922176b 100644 --- a/tests/setup/test_env.py +++ b/tests/setup/test_env.py @@ -3,16 +3,52 @@ from pathlib import Path from dotenv import load_dotenv -# Load .env.test from the tests directory (one level up from setup/) -test_env_path = Path(__file__).resolve().parents[1] / ".env.test" -load_dotenv(test_env_path) +# Determine environment type: 'test' (default) or 'normal' +ENV_TYPE = os.getenv('ENV_TYPE', 'test') + +# Project paths - absolute paths that work from any working directory +PROJECT_ROOT = Path(__file__).resolve().parents[2] # friend-lite root +TESTS_DIR = Path(__file__).resolve().parents[1] # tests directory +BACKENDS_ADVANCED_DIR = PROJECT_ROOT / "backends" / "advanced" + +# Load appropriate .env file based on ENV_TYPE +if ENV_TYPE == 'normal': + env_file = TESTS_DIR / ".env.normal" + # Also load the main .env from backends/advanced + main_env = BACKENDS_ADVANCED_DIR / ".env" + load_dotenv(main_env) # Load main env first + load_dotenv(env_file, override=True) # Override with test-specific settings +else: # test (default) + env_file = TESTS_DIR / ".env.test" + load_dotenv(env_file) + +# Convert to strings for Robot Framework +PROJECT_ROOT_STR = str(PROJECT_ROOT) +BACKENDS_ADVANCED_DIR_STR = str(BACKENDS_ADVANCED_DIR) + +# Environment-specific configurations +if ENV_TYPE == 'normal': + # Normal environment (production-like) + API_PORT = '8000' + FRONTEND_PORT = '5173' + COMPOSE_FILE = 'docker-compose.yml' + MONGO_PORT = '27017' + QDRANT_PORT = '6333' +else: # test + # Test environment (isolated) + API_PORT = '8001' + FRONTEND_PORT = '3001' + COMPOSE_FILE = 'docker-compose-test.yml' + MONGO_PORT = '27018' + QDRANT_PORT = '6337' # API Configuration -API_URL = 'http://localhost:8001' # Use BACKEND_URL from test.env -API_BASE = 'http://localhost:8001/api' +API_URL = os.getenv('BACKEND_URL', f'http://localhost:{API_PORT}') +API_BASE = f'{API_URL}/api' SPEAKER_RECOGNITION_URL = 'http://localhost:8085' # Speaker recognition service -WEB_URL = os.getenv('FRONTEND_URL', 'http://localhost:3001') # Use FRONTEND_URL from test.env +WEB_URL = os.getenv('FRONTEND_URL', f'http://localhost:{FRONTEND_PORT}') + # Admin user credentials (Robot Framework format) ADMIN_USER = { "email": os.getenv('ADMIN_EMAIL', 'test-admin@example.com'), From 4e530cba10f8a3853c80348e886bff2f3bed1e25 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Tue, 9 Dec 2025 22:02:59 +0000 Subject: [PATCH 13/21] added install docs --- Docs/setup/linux.md | 564 ++++++++++++++ Docs/setup/macos.md | 392 ++++++++++ Docs/setup/prerequisites.md | 365 +++++++++ Docs/setup/tailscale.md | 708 ++++++++++++++++++ Docs/setup/windows-gitbash.md | 714 ++++++++++++++++++ Docs/setup/windows-wsl2.md | 738 +++++++++++++++++++ backends/advanced/compose/infrastructure.yml | 57 -- backends/advanced/compose/mycelia.yml | 9 - compose/advanced-backend.yml | 7 - 9 files changed, 3481 insertions(+), 73 deletions(-) create mode 100644 Docs/setup/linux.md create mode 100644 Docs/setup/macos.md create mode 100644 Docs/setup/prerequisites.md create mode 100644 Docs/setup/tailscale.md create mode 100644 Docs/setup/windows-gitbash.md create mode 100644 Docs/setup/windows-wsl2.md delete mode 100644 backends/advanced/compose/infrastructure.yml delete mode 100644 backends/advanced/compose/mycelia.yml delete mode 100644 compose/advanced-backend.yml diff --git a/Docs/setup/linux.md b/Docs/setup/linux.md new file mode 100644 index 00000000..31c9e94b --- /dev/null +++ b/Docs/setup/linux.md @@ -0,0 +1,564 @@ +# Chronicle Setup Guide - Linux + +**Quick installation guide for Linux users (Ubuntu/Debian).** + +Linux provides the best performance for Chronicle with native Docker support. + +--- + +## Prerequisites + +Before starting: +- โœ… Read [Prerequisites Guide](prerequisites.md) and have your API keys ready +- โœ… **Ubuntu 20.04+** or **Debian 11+** (or compatible distribution) +- โœ… **At least 20GB free disk space** +- โœ… **8GB RAM** (4GB minimum) +- โœ… **sudo access** + +--- + +## Quick Install (TL;DR) + +For experienced users: + +```bash +# Install dependencies +curl -fsSL https://raw.githubusercontent.com/BasedHardware/Friend/main/scripts/install-deps.sh | bash + +# Log out and back in (for docker group) +# Or run: newgrp docker + +# Clone and setup +git clone https://github.com/BasedHardware/Friend.git chronicle +cd chronicle +make wizard + +# Start Chronicle +./start-env.sh dev +``` + +Access at: http://localhost:3010 + +--- + +## Detailed Installation + +### Step 1: Install Dependencies Automatically + +We provide a script that installs everything: + +```bash +curl -fsSL https://raw.githubusercontent.com/BasedHardware/Friend/main/scripts/install-deps.sh | bash +``` + +**You'll be asked for your sudo password.** + +**What this installs:** +- Git (version control) +- Make (build automation) +- curl, wget (download tools) +- Docker Engine (container runtime) +- Docker Compose (multi-container orchestration) + +**Docker installation:** + +The script will: +1. Add Docker's official GPG key +2. Set up Docker repository +3. Install Docker Engine and Docker Compose +4. Add you to the `docker` group +5. Start Docker service + +**After installation:** + +``` +โš ๏ธ IMPORTANT: You need to log out and log back in for group changes to take effect + Or run: newgrp docker +``` + +**Option 1 (Recommended): Log out and back in** +- Click user menu โ†’ Log Out +- Log back in +- Your user now has docker permissions + +**Option 2: Use newgrp (temporary)** +```bash +newgrp docker +``` +- Only affects current terminal session +- Need to run in every new terminal + +**Verify installation:** +```bash +git --version +make --version +docker --version +docker compose version +docker ps # Should work without sudo +``` + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 2: Install Tailscale (Optional but Recommended) + +Tailscale enables remote access from your phone or anywhere. + +```bash +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up +``` + +Follow login prompts in your browser. + +**Detailed instructions**: See [Tailscale Setup Guide](tailscale.md) + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 3: Install Chronicle + +#### 3.1 Clone the Repository + +```bash +cd ~ +git clone https://github.com/BasedHardware/Friend.git chronicle +cd chronicle +``` + +#### 3.2 Run Setup Wizard + +```bash +make wizard +``` + +The wizard will guide you through configuration. **Have your API keys ready from the [Prerequisites Guide](prerequisites.md).** + +**Secrets:** +- JWT secret: Press Enter (auto-generates) +- Admin email: Press Enter or type your email +- Admin password: Type a secure password +- OpenAI API key: Paste your key +- Deepgram API key: Paste your key +- Optional keys: Press Enter to skip + +**Tailscale (if installed):** +- Configure Tailscale?: `y` if you installed in Step 2 +- SSL option: Choose `1` for automatic HTTPS + +**Environment:** +- Environment name: Press Enter (use "dev") +- Port offset: Press Enter +- Database names: Press Enter for defaults +- Optional services: `N` for all (enable later if needed) + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 4: Start Chronicle + +```bash +./start-env.sh dev +``` + +**First-time startup:** +- Downloads Docker images (~2GB, takes 5-10 minutes) +- Builds Chronicle services +- Starts 5 containers + +**You'll see:** +``` +โœ… Services Started Successfully! + +๐ŸŒ Access Your Services: + + ๐Ÿ“ฑ Web Dashboard: http://localhost:3010 + ๐Ÿ”Œ Backend API: http://localhost:8000 + ๐Ÿ“š API Docs: http://localhost:8000/docs +``` + +**Verify services are running:** +```bash +docker ps +``` + +Should show 5 containers in "Up" status. + +โฑ๏ธ **Time: 10 minutes (first time)** + +--- + +### Step 5: Access Chronicle + +1. Open browser: **http://localhost:3010** +2. Log in with your admin credentials +3. Start using Chronicle! + +**Check API docs:** +- http://localhost:8000/docs + +๐ŸŽ‰ **Installation complete!** + +โฑ๏ธ **Time: 2 minutes** + +--- + +## Managing Chronicle + +### Start/Stop/Restart + +```bash +cd ~/chronicle + +# Start +./start-env.sh dev + +# Stop +docker compose down + +# Restart +docker compose restart + +# View logs +docker compose logs -f # Press Ctrl+C to exit +``` + +### Check Status + +```bash +# Service status +docker compose ps + +# System resources +docker stats + +# Disk usage +docker system df +``` + +### Update Chronicle + +```bash +cd ~/chronicle +git pull +docker compose up -d --build +``` + +--- + +## Troubleshooting + +### "Permission denied" when running docker + +**You need to log out and back in** after installation. + +Or temporarily: +```bash +newgrp docker +``` + +Or check if you're in docker group: +```bash +groups | grep docker +``` + +### "Cannot connect to Docker daemon" + +**Start Docker service:** +```bash +sudo systemctl start docker +sudo systemctl enable docker # Start on boot +``` + +**Check status:** +```bash +sudo systemctl status docker +``` + +### Port conflicts (8000, 3010 in use) + +**Find process using port:** +```bash +sudo lsof -i :8000 +sudo lsof -i :3010 +``` + +**Kill process:** +```bash +sudo kill -9 +``` + +**Or use different ports:** +Edit `environments/dev.env`: +```bash +PORT_OFFSET=100 +``` + +### Docker installation failed + +**Manual installation:** +```bash +# Remove any old versions +sudo apt-get remove docker docker-engine docker.io containerd runc + +# Install dependencies +sudo apt-get update +sudo apt-get install -y ca-certificates curl gnupg lsb-release + +# Add Docker GPG key +sudo mkdir -p /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg + +# Add Docker repository +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +# Install Docker +sudo apt-get update +sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +# Add user to docker group +sudo usermod -aG docker $USER + +# Start Docker +sudo systemctl start docker +sudo systemctl enable docker +``` + +Then log out and back in. + +--- + +## Distribution-Specific Notes + +### Ubuntu 22.04 LTS (Recommended) + +Works perfectly out of the box. This is our primary test platform. + +### Ubuntu 20.04 LTS + +Fully supported. May need to update Docker Compose: +```bash +sudo apt-get update +sudo apt-get install docker-compose-plugin +``` + +### Debian 11/12 + +Fully supported. Use the install script or follow manual installation. + +### Fedora/RHEL/CentOS + +**Install script:** +```bash +curl -fsSL https://raw.githubusercontent.com/BasedHardware/Friend/main/scripts/install-deps.sh | bash +``` + +**Or manual:** +```bash +sudo dnf install -y git make docker docker-compose +sudo systemctl start docker +sudo systemctl enable docker +sudo usermod -aG docker $USER +``` + +### Arch Linux + +```bash +sudo pacman -S git make docker docker-compose +sudo systemctl start docker +sudo systemctl enable docker +sudo usermod -aG docker $USER +``` + +--- + +## Performance Optimization + +### Increase Docker Performance + +**Use overlay2 storage driver** (default on modern systems): +```bash +docker info | grep "Storage Driver" +``` + +Should show `overlay2`. + +**Limit Docker log size** (prevents disk filling): +```bash +sudo nano /etc/docker/daemon.json +``` + +Add: +```json +{ + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3" + } +} +``` + +Restart Docker: +```bash +sudo systemctl restart docker +``` + +### System Resources + +**Check available resources:** +```bash +# RAM +free -h + +# Disk space +df -h + +# CPU +nproc +``` + +**Recommended for Chronicle:** +- 8GB RAM (4GB minimum) +- 4 CPU cores (2 minimum) +- 20GB disk space + +--- + +## Security Notes + +### Firewall Configuration + +If you have a firewall enabled: + +```bash +# Allow Chronicle ports (local access only) +sudo ufw allow from 127.0.0.1 to any port 8000 +sudo ufw allow from 127.0.0.1 to any port 3010 + +# For Tailscale remote access, no firewall rules needed +# Tailscale handles secure connections +``` + +### Keep System Updated + +```bash +# Update system packages +sudo apt-get update +sudo apt-get upgrade + +# Update Docker +sudo apt-get upgrade docker-ce docker-ce-cli containerd.io +``` + +--- + +## Advanced Tips + +### Run Chronicle on Boot + +**Create systemd service:** +```bash +sudo nano /etc/systemd/system/chronicle.service +``` + +Add: +```ini +[Unit] +Description=Chronicle +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=/home/yourusername/chronicle +ExecStart=/home/yourusername/chronicle/start-env.sh dev +ExecStop=/usr/bin/docker compose down +User=yourusername + +[Install] +WantedBy=multi-user.target +``` + +Enable: +```bash +sudo systemctl daemon-reload +sudo systemctl enable chronicle +sudo systemctl start chronicle +``` + +### Monitor Resource Usage + +```bash +# Real-time container stats +docker stats + +# System monitoring +htop # Install with: sudo apt-get install htop + +# Disk usage by container +docker system df -v +``` + +### Remote Server Setup + +If running on a VPS/cloud server: + +1. **Install Tailscale** for secure access (recommended) +2. **Or configure firewall** for specific IP access: + ```bash + sudo ufw allow from YOUR_IP to any port 8000 + sudo ufw allow from YOUR_IP to any port 3010 + ``` +3. **Use HTTPS** (Tailscale provides this automatically) + +--- + +## Next Steps + +1. **Configure mobile app** - Connect your phone/OMI device +2. **Test audio upload** - Try uploading test audio +3. **Explore API** - Visit http://localhost:8000/docs +4. **Read documentation** - Check `CLAUDE.md` +5. **Set up Tailscale** - For remote access (if not done) + +--- + +## Getting Help + +- **GitHub Issues**: https://github.com/BasedHardware/Friend/issues +- **Docker Docs**: https://docs.docker.com/engine/install/ +- **Ubuntu Help**: https://help.ubuntu.com/ + +--- + +## Summary + +โœ… **What you installed:** +- Docker Engine (native, no Desktop needed) +- Docker Compose plugin +- Chronicle and all dependencies +- (Optional) Tailscale + +โœ… **Linux advantages:** +- **Best performance** - Native Docker, no virtualization +- **Lightest weight** - No GUI overhead +- **Most stable** - Industry-standard deployment platform +- **Free forever** - No licensing concerns +- **Server-ready** - Perfect for VPS/cloud deployment + +โœ… **What you can do:** +- Access dashboard: http://localhost:3010 +- Access API: http://localhost:8000 +- Run as system service (auto-start on boot) +- Deploy to production servers + +**Total setup time: ~20-30 minutes** + +Welcome to Chronicle on Linux! ๐Ÿง๐Ÿš€ diff --git a/Docs/setup/macos.md b/Docs/setup/macos.md new file mode 100644 index 00000000..6e6a1c03 --- /dev/null +++ b/Docs/setup/macos.md @@ -0,0 +1,392 @@ +# Chronicle Setup Guide - macOS + +**Quick installation guide for macOS users.** + +macOS has excellent support for Chronicle with native Docker and Unix tools. + +--- + +## Prerequisites + +Before starting: +- โœ… Read [Prerequisites Guide](prerequisites.md) and have your API keys ready +- โœ… **macOS 10.15 (Catalina) or higher** +- โœ… **At least 20GB free disk space** +- โœ… **8GB RAM** (4GB minimum) +- โœ… **Administrator access** + +--- + +## Quick Install (TL;DR) + +For experienced users: + +```bash +# Install dependencies +curl -fsSL https://raw.githubusercontent.com/BasedHardware/Friend/main/scripts/install-deps.sh | bash + +# Clone and setup +git clone https://github.com/BasedHardware/Friend.git chronicle +cd chronicle +make wizard + +# Start Chronicle +./start-env.sh dev +``` + +Access at: http://localhost:3010 + +--- + +## Detailed Installation + +### Step 1: Install Homebrew (if needed) + +Homebrew is macOS's package manager. Check if you have it: + +```bash +brew --version +``` + +**If you see a version number**, skip to Step 2. + +**If "command not found"**, install Homebrew: + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +Follow the prompts: +- Press `Enter` to continue +- Enter your Mac password when asked +- Wait for installation (~5 minutes) + +**After installation**, run the commands it shows to add Homebrew to your PATH. + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 2: Install Dependencies Automatically + +We have a script that installs everything you need: + +```bash +curl -fsSL https://raw.githubusercontent.com/BasedHardware/Friend/main/scripts/install-deps.sh | bash +``` + +**What this installs:** +- Git (version control) +- Make (build tool) +- curl (HTTP client) +- Docker Desktop (container platform) + +**Docker Desktop installation:** + +The script will ask: +``` +Install Docker Desktop via Homebrew? (y/N): +``` + +**Option 1: Type `y`** - Installs automatically via Homebrew +**Option 2: Type `N`** - Download manually from docker.com + +**If you choose automatic install:** +- Wait for Homebrew to download and install Docker Desktop (~10 minutes) +- After install: Open Docker Desktop from Applications +- Accept the service agreement +- Docker will start (whale icon in menu bar) + +**Verify installation:** +```bash +git --version +make --version +docker --version +docker compose version +``` + +All should show version numbers. + +โฑ๏ธ **Time: 15 minutes** + +--- + +### Step 3: Install Tailscale (Optional but Recommended) + +Tailscale enables remote access to Chronicle from your phone or anywhere. + +**Quick install:** +```bash +brew install tailscale +sudo brew services start tailscale +sudo tailscale up +``` + +Follow login prompts in your browser. + +**Detailed instructions**: See [Tailscale Setup Guide](tailscale.md) + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 4: Install Chronicle + +#### 4.1 Clone the Repository + +```bash +cd ~ +git clone https://github.com/BasedHardware/Friend.git chronicle +cd chronicle +``` + +#### 4.2 Run Setup Wizard + +```bash +make wizard +``` + +The wizard will ask you questions. **Have your API keys ready from the [Prerequisites Guide](prerequisites.md).** + +**Secrets Configuration:** +- JWT secret: Press Enter (auto-generates) +- Admin email: Press Enter or enter your email +- Admin password: Enter a secure password +- OpenAI API key: Paste your key +- Deepgram API key: Paste your key +- Optional keys: Press Enter to skip + +**Tailscale (if installed):** +- Configure Tailscale?: Type `y` if you installed it in Step 3 +- Choose option 1 for automatic HTTPS + +**Environment:** +- Environment name: Press Enter (use "dev") +- Port offset: Press Enter (use default) +- Database names: Press Enter for defaults +- Optional services: Type `N` for all (can enable later) + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 5: Start Chronicle + +```bash +./start-env.sh dev +``` + +**First-time startup:** +- Downloads Docker images (~5 minutes) +- Builds services +- Starts 5 containers + +**You'll see:** +``` +โœ… Services Started Successfully! + +๐ŸŒ Access Your Services: + + ๐Ÿ“ฑ Web Dashboard: http://localhost:3010 + ๐Ÿ”Œ Backend API: http://localhost:8000 + ๐Ÿ“š API Docs: http://localhost:8000/docs +``` + +โฑ๏ธ **Time: 10 minutes (first time)** + +--- + +### Step 6: Access Chronicle + +1. Open browser: **http://localhost:3010** +2. Log in with your admin credentials +3. Explore the dashboard! + +**Check API documentation:** +- http://localhost:8000/docs + +๐ŸŽ‰ **You're all set!** + +โฑ๏ธ **Time: 2 minutes** + +--- + +## Managing Chronicle + +### Start/Stop/Restart + +```bash +cd ~/chronicle + +# Start +./start-env.sh dev + +# Stop +docker compose down + +# Restart +docker compose restart + +# View logs +docker compose logs -f +``` + +### Update Chronicle + +```bash +cd ~/chronicle +git pull +docker compose up -d --build +``` + +### Check Status + +```bash +docker compose ps +``` + +--- + +## Troubleshooting + +### "docker: command not found" + +**Make sure Docker Desktop is running:** +1. Open Spotlight (Cmd+Space) +2. Type "Docker" +3. Press Enter +4. Wait for whale icon in menu bar + +### "Permission denied" when running docker + +**Add yourself to docker group:** +```bash +sudo dscl . append /Groups/docker GroupMembership $USER +``` + +Then log out and back in. + +### Port conflicts (8000, 3010 in use) + +**Find what's using the port:** +```bash +lsof -i :8000 +``` + +**Kill the process:** +```bash +kill -9 +``` + +**Or use different ports:** +Edit `environments/dev.env`: +```bash +PORT_OFFSET=100 +``` + +### Homebrew installation fails + +**Update Command Line Tools:** +```bash +xcode-select --install +``` + +Then retry Homebrew installation. + +--- + +## macOS-Specific Tips + +### Use iTerm2 for Better Terminal + +```bash +brew install --cask iterm2 +``` + +iTerm2 has better features than default Terminal. + +### Keyboard Shortcuts + +- `Cmd+Space`: Open Spotlight (quick app launcher) +- `Cmd+Tab`: Switch between apps +- `Cmd+Shift+.`: Show hidden files in Finder + +### Access Chronicle Files in Finder + +```bash +open ~/chronicle +``` + +Opens the folder in Finder. + +### VS Code Integration + +```bash +brew install --cask visual-studio-code +cd ~/chronicle +code . +``` + +Opens Chronicle in VS Code. + +--- + +## Performance Notes + +### Apple Silicon (M1/M2/M3) + +Docker Desktop has **excellent performance** on Apple Silicon: +- โœ… Native ARM support +- โœ… Fast container startup +- โœ… Low resource usage + +### Intel Macs + +Docker Desktop uses virtualization: +- Still good performance +- May use more RAM +- Consider closing other apps during heavy use + +--- + +## Next Steps + +1. **Configure mobile app** - Connect your phone or OMI device +2. **Test with audio** - Upload test audio file +3. **Explore settings** - Check Settings โ†’ Memory Provider +4. **Read documentation** - Check `CLAUDE.md` +5. **Set up Tailscale** - For remote access (if not done yet) + +--- + +## Getting Help + +- **GitHub Issues**: https://github.com/BasedHardware/Friend/issues +- **Docker for Mac**: https://docs.docker.com/desktop/mac/ +- **Homebrew Docs**: https://docs.brew.sh/ + +--- + +## Summary + +โœ… **What you installed:** +- Homebrew (package manager) +- Docker Desktop for Mac +- Chronicle and dependencies +- (Optional) Tailscale + +โœ… **What you can do:** +- Access dashboard: http://localhost:3010 +- Access API: http://localhost:8000 +- Manage containers via Docker Desktop GUI +- Use native macOS tools + +โœ… **macOS advantages:** +- Native Unix environment (no WSL needed) +- Excellent Docker Desktop support +- Great performance (especially on Apple Silicon) +- Easy package management with Homebrew + +**Total setup time: ~30-45 minutes** + +Welcome to Chronicle on macOS! ๐ŸŽ๐Ÿš€ diff --git a/Docs/setup/prerequisites.md b/Docs/setup/prerequisites.md new file mode 100644 index 00000000..9d7c41aa --- /dev/null +++ b/Docs/setup/prerequisites.md @@ -0,0 +1,365 @@ +# Chronicle Prerequisites + +This guide covers the software and accounts you need **before** installing Chronicle, regardless of your operating system. + +--- + +## System Requirements + +### Minimum +- **CPU**: 2 cores +- **RAM**: 4GB +- **Disk**: 10GB free space +- **OS**: + - Windows 10 (version 2004+) or Windows 11 + - macOS 10.15 (Catalina) or higher + - Ubuntu 20.04+ or Debian 11+ + +### Recommended +- **CPU**: 4+ cores +- **RAM**: 8GB+ +- **Disk**: 20GB+ free space +- **SSD** for better Docker performance + +### For Speaker Recognition (Optional) +- **RAM**: 8GB+ recommended +- **GPU**: NVIDIA GPU with CUDA support (optional, improves performance) + +--- + +## Required Software + +Chronicle requires these tools to run. The installation process varies by platform (see platform-specific guides). + +### 1. Docker & Docker Compose + +**What it does**: Runs Chronicle's services in isolated containers (database, backend, web UI, etc.) + +**Version required**: +- Docker 20.10.0 or higher +- Docker Compose v2.0.0 or higher (plugin version) + +**Installation**: See your platform-specific guide +- [Windows with WSL2](windows-wsl2.md) +- [Windows with Git Bash](windows-gitbash.md) +- [macOS](macos.md) +- [Linux](linux.md) + +### 2. Git + +**What it does**: Downloads Chronicle code from GitHub + +**Version required**: Git 2.0 or higher + +**Verification**: +```bash +git --version +``` + +### 3. Make + +**What it does**: Runs the setup wizard and management commands + +**Version required**: Make 3.81 or higher + +**Verification**: +```bash +make --version +``` + +### 4. Bash Shell + +**What it does**: Runs Chronicle's setup scripts + +**Required**: Bash 4.0 or higher + +**Verification**: +```bash +bash --version +``` + +**Platform notes**: +- **macOS/Linux**: Pre-installed โœ… +- **Windows**: Use WSL2 (recommended) or Git Bash + +### 5. OpenSSL + +**What it does**: Generates secure JWT tokens and SSL certificates + +**Version required**: OpenSSL 1.1.1 or higher + +**Verification**: +```bash +openssl version +``` + +--- + +## Required API Keys + +You'll need these API keys to use Chronicle's core features. Have them ready before running the setup wizard. + +### 1. OpenAI API Key (Required) + +**Used for**: Extracting memories from your conversations + +**Sign up**: https://platform.openai.com/signup + +**Get your key**: +1. Go to https://platform.openai.com/api-keys +2. Click "+ Create new secret key" +3. Give it a name (e.g., "Chronicle") +4. Copy the key (starts with `sk-proj-...`) +5. **Save it securely** - you can only see it once! + +**Cost**: Pay-as-you-go, typically $1-5/month for personal use + +**Model used**: `gpt-4o-mini` (fast and affordable) + +### 2. Deepgram API Key (Required) + +**Used for**: Converting speech to text (transcription) + +**Sign up**: https://console.deepgram.com/signup + +**Get your key**: +1. Go to https://console.deepgram.com/ +2. Click "API Keys" in left sidebar +3. You'll see a default API key already created +4. Click the ๐Ÿ‘๏ธ eye icon to reveal it +5. Copy the key + +**Cost**: Free tier includes $200 credit (plenty for testing) + +**Why Deepgram**: High-quality, real-time transcription with speaker diarization + +--- + +## Optional API Keys + +These enhance Chronicle but aren't required to get started. + +### Mistral API Key (Optional) + +**Used for**: Alternative transcription service (Voxtral models) + +**Sign up**: https://console.mistral.ai/ + +**Get your key**: +1. Go to https://console.mistral.ai/api-keys +2. Create a new API key +3. Copy and save it + +**When to use**: Alternative to Deepgram, supports Voxtral transcription models + +### Hugging Face Token (Optional) + +**Used for**: Speaker recognition models + +**Sign up**: https://huggingface.co/join + +**Get your token**: +1. Go to https://huggingface.co/settings/tokens +2. Click "New token" +3. Choose "Read" access +4. Copy the token (starts with `hf_...`) + +**When to use**: If you want to identify different speakers in conversations + +### Groq API Key (Optional) + +**Used for**: Alternative LLM provider (faster inference) + +**Sign up**: https://console.groq.com/ + +**When to use**: Alternative to OpenAI for memory extraction + +--- + +## Optional Software + +### Tailscale (Recommended) + +**What it does**: Creates a secure network so you can access Chronicle remotely + +**Why you want this**: +- Access your Chronicle dashboard from anywhere +- Connect your phone/OMI device when away from home +- No need to open firewall ports or configure port forwarding + +**Sign up**: https://login.tailscale.com/start + +**Installation**: See [Tailscale Setup Guide](tailscale.md) + +**When to install**: +- **Windows/Linux**: Install before running `make wizard` +- **macOS**: Can install anytime + +**Cost**: Free for personal use (up to 100 devices) + +### Python 3.8+ (Optional) + +**What it does**: Needed for development and testing outside Docker + +**When you need it**: +- Running tests locally +- Backend development without Docker +- Using management scripts + +**Most users don't need this** - Chronicle runs in Docker containers. + +--- + +## Account Setup Summary + +Before running `make wizard`, have these ready: + +### Required โœ… +- [ ] OpenAI API key +- [ ] Deepgram API key +- [ ] Admin password (choose a secure password) + +### Recommended ๐ŸŒŸ +- [ ] Tailscale account (for remote access) + +### Optional ๐Ÿ”ง +- [ ] Mistral API key (alternative transcription) +- [ ] Hugging Face token (speaker recognition) +- [ ] Groq API key (alternative LLM) + +--- + +## Cost Breakdown + +### One-Time Costs +- **$0** - All required software is free and open source + +### Ongoing Costs (Pay-as-you-go) + +**OpenAI (Required)**: +- ~$1-5/month for typical personal use +- Depends on number of conversations and memory processing + +**Deepgram (Required)**: +- $200 free credit (lasts months for personal use) +- After credit: ~$0.0043/minute of audio +- Example: 1 hour of audio/day = ~$7.74/month + +**Tailscale (Recommended)**: +- **Free** for personal use (up to 100 devices) + +**Total typical monthly cost**: $1-12/month depending on usage + +### Free Alternative + +You can run completely free using: +- **Parakeet ASR** (offline transcription, runs on your computer) +- **Ollama** (local LLM, runs on your computer) + +**Trade-off**: Requires more powerful hardware and longer processing times. + +--- + +## Network Requirements + +### Ports Used (Local Network Only) + +By default, Chronicle uses these ports on `localhost`: +- **8000** - Backend API +- **5173** - Web dashboard +- **27017** - MongoDB +- **6379** - Redis +- **6333/6334** - Qdrant vector database + +**Firewall**: No incoming port forwarding needed if using Tailscale + +### Internet Bandwidth + +**Minimal** - Most processing happens locally: +- API calls to OpenAI/Deepgram (small data transfers) +- Audio upload from devices (depends on recording frequency) +- Typical usage: <100MB/day + +--- + +## Verification Checklist + +Before proceeding to installation, verify you have: + +### Software (will install in next steps) +- [ ] Docker installation method chosen +- [ ] Know how to open terminal/command prompt on your OS +- [ ] Have administrator/sudo access on your computer + +### Accounts & Keys +- [ ] OpenAI account created and API key saved +- [ ] Deepgram account created and API key saved +- [ ] Admin password chosen (8+ characters, secure) +- [ ] (Optional) Tailscale account created + +### System +- [ ] 20GB+ free disk space +- [ ] 8GB+ RAM (4GB minimum) +- [ ] Internet connection working + +--- + +## Next Steps + +Once you have all prerequisites: + +1. Choose your platform-specific guide: + - **Windows**: [WSL2 Setup](windows-wsl2.md) or [Git Bash Setup](windows-gitbash.md) + - **macOS**: [macOS Setup](macos.md) + - **Linux**: [Linux Setup](linux.md) + +2. (Optional) Install Tailscale: [Tailscale Setup Guide](tailscale.md) + +3. Install Chronicle dependencies and run the wizard + +4. Start using Chronicle! + +--- + +## Getting Help + +### Can't get an API key? + +**OpenAI requires a credit card** - If this is an issue: +- Consider using the free local alternative (Ollama) +- Share an account with a friend/family member + +**Deepgram free credit** - Should be automatically applied on signup: +- Check your console dashboard at https://console.deepgram.com/ +- Contact Deepgram support if credit not showing + +### Don't have the hardware? + +**Minimum 4GB RAM** might not be enough for all features: +- Start with cloud services (OpenAI + Deepgram) +- Disable optional services (speaker recognition, Mycelia) +- Consider a cloud VPS (Digital Ocean, Linode) for $10-20/month + +### Questions? + +- **GitHub Issues**: https://github.com/BasedHardware/Friend/issues +- **Documentation**: Check other guides in `docs/` +- **Community**: (Add Discord/community link if available) + +--- + +## Summary + +**Minimum to get started**: +1. A computer with 4GB+ RAM and 10GB+ disk space +2. OpenAI API key (~$1-5/month) +3. Deepgram API key (free $200 credit) +4. 30-60 minutes for installation + +**Recommended setup**: +1. A computer with 8GB+ RAM and 20GB+ disk space +2. OpenAI and Deepgram API keys +3. Tailscale for remote access (free) +4. 60-90 minutes for full installation including Tailscale + +Ready to install? Choose your platform guide and let's go! ๐Ÿš€ diff --git a/Docs/setup/tailscale.md b/Docs/setup/tailscale.md new file mode 100644 index 00000000..4473fdc4 --- /dev/null +++ b/Docs/setup/tailscale.md @@ -0,0 +1,708 @@ +# Tailscale Setup for Chronicle + +**Complete guide to setting up Tailscale for remote access to Chronicle.** + +Tailscale creates a secure private network so you can access Chronicle from anywhere - your phone, other computers, even when away from home. + +--- + +## What is Tailscale? + +**Tailscale** is a zero-config VPN that creates a secure network between your devices. + +### Why Use Tailscale with Chronicle? + +โœ… **Access from anywhere** - Use Chronicle from your phone when away from home +โœ… **No port forwarding** - No need to open firewall ports or configure your router +โœ… **Automatic HTTPS** - Built-in SSL certificates with `tailscale serve` +โœ… **Zero configuration** - Works automatically once installed +โœ… **Free for personal use** - Up to 100 devices +โœ… **Secure** - End-to-end encrypted connections + +### How It Works + +``` +Your Phone (anywhere) + โ†“ + Tailscale Network (secure tunnel) + โ†“ +Your Home Computer (Chronicle) +``` + +Instead of `http://localhost:3010`, you access: +`https://your-computer.tail12345.ts.net` + +--- + +## โš ๏ธ Important: You Need Tailscale on BOTH Devices + +To access Chronicle remotely, you must install Tailscale on: + +1. **Your Computer** - Where Chronicle is running (Step 2) +2. **Your Phone/Tablet** - The device you'll use to access Chronicle remotely (Step 5) + +Both devices must be connected to the same Tailscale account to communicate securely. + +--- + +## Prerequisites + +- โœ… Chronicle installed (or in process of installing) +- โœ… Internet connection +- โœ… A phone/tablet to access Chronicle from (iPhone, iPad, or Android) +- โœ… Your computer where Chronicle runs + +--- + +## Quick Setup Overview + +**Here's what you'll do:** + +1. โœ… Create ONE Tailscale account +2. โœ… Install Tailscale on your **computer** (where Chronicle runs) +3. โœ… Install Tailscale on your **phone/tablet** (to access Chronicle remotely) +4. โœ… Connect both devices to your Tailscale account +5. โœ… Configure Chronicle to use Tailscale +6. โœ… Test access from your phone + +**Total time:** ~20 minutes + +--- + +## Step 1: Create Tailscale Account + +### 1.1 Sign Up + +1. Go to: https://login.tailscale.com/start +2. Choose sign-up method: + - **Google** account (easiest) + - **Microsoft** account + - **GitHub** account + - **Email** (create new Tailscale account) +3. Complete sign-up process + +โœ… **Account created!** + +--- + +## Step 2: Install on Your Computer + +Choose your operating system: + +### For Windows (WSL2 or Native) + +**If using WSL2 (recommended):** + +Open Ubuntu terminal and run: +```bash +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up +``` + +**If using native Windows:** + +1. Download: https://tailscale.com/download/windows +2. Run `tailscale-setup-.exe` +3. Click "Install" +4. Log in when prompted + +### For macOS + +**Option 1: Graphical installer (recommended)** +1. Download: https://tailscale.com/download/mac +2. Open `Tailscale-.pkg` +3. Click through installer +4. Launch Tailscale from Applications +5. Click "Log in" in menu bar + +**Option 2: Homebrew** +```bash +brew install tailscale +sudo brew services start tailscale +sudo tailscale up +``` + +### For Linux + +```bash +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up +``` + +**Or for specific distros:** + +**Ubuntu/Debian:** +```bash +curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null +curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list +sudo apt-get update +sudo apt-get install tailscale +sudo tailscale up +``` + +**Fedora/RHEL:** +```bash +sudo dnf config-manager --add-repo https://pkgs.tailscale.com/stable/fedora/tailscale.repo +sudo dnf install tailscale +sudo systemctl enable --now tailscaled +sudo tailscale up +``` + +--- + +## Step 3: Connect Your Computer + +### 3.1 Start Tailscale + +When you run `sudo tailscale up`, you'll see: + +``` +To authenticate, visit: + + https://login.tailscale.com/a/1234567890 + +``` + +### 3.2 Authenticate + +1. **Copy the URL** shown in terminal +2. **Paste in your browser** +3. **Log in** with the account you created in Step 1 +4. **Click "Connect"** or "Authorize" + +You should see: +``` +Success. +``` + +### 3.3 Verify Connection + +**Check status:** +```bash +tailscale status +``` + +You should see: +``` +# Tailscale status: +my-computer 100.x.x.x my-computer.tail12345.ts.net +``` + +โœ… **Your computer is now on Tailscale!** + +--- + +## Step 4: Get Your Tailscale Hostname + +You'll need this for Chronicle configuration. + +### 4.1 Find Your Hostname + +**Method 1: Command line** +```bash +tailscale status --json | grep DNSName +``` + +**Method 2: Web dashboard** +1. Go to: https://login.tailscale.com/admin/machines +2. Find your computer in the list +3. The hostname is shown (e.g., `my-computer.tail12345.ts.net`) + +**Method 3: Simple command** +```bash +tailscale status | head -1 | awk '{print $3}' +``` + +**Example hostnames:** +- `laptop.tail12345.ts.net` +- `desktop-pc.tail67890.ts.net` +- `my-server.tailabcde.ts.net` + +**Write this down!** You'll need it for Chronicle setup. + +--- + +## Step 5: Install Tailscale on Your Phone/Tablet + +**โš ๏ธ REQUIRED: You must install Tailscale on your phone/tablet to access Chronicle remotely.** + +Without Tailscale on your mobile device, you won't be able to connect to Chronicle when away from home. + +### For iPhone/iPad + +1. **Open App Store** +2. **Search "Tailscale"** +3. **Install** the Tailscale app (free) +4. **Open** the app +5. **Tap "Log in"** +6. **Log in with the SAME account** you used in Step 1 (very important!) +7. โœ… Your device is now connected! + +**App Store link**: https://apps.apple.com/app/tailscale/id1470499037 + +### For Android + +1. **Open Google Play Store** +2. **Search "Tailscale"** +3. **Install** the Tailscale app (free) +4. **Open** the app +5. **Tap "Sign in"** +6. **Log in with the SAME account** you used in Step 1 (very important!) +7. โœ… Your device is now connected! + +**Play Store link**: https://play.google.com/store/apps/details?id=com.tailscale.ipn + +### โœ… Verify Both Devices Are Connected + +**On your phone's Tailscale app:** +1. You should see **your computer listed** (e.g., "my-laptop") +2. It should show as **"connected"** with a **green dot** โœ… + +**On your computer:** +```bash +tailscale status +``` +You should see **your phone listed** in the output. + +**Both devices must be connected for remote access to work!** + +--- + +## Step 6: Configure Chronicle for Tailscale + +Now we need to tell Chronicle about your Tailscale hostname. + +### Option A: During Initial Setup (Recommended) + +When running `make wizard`, you'll be asked: + +``` +Do you want to configure Tailscale? (y/N): +``` + +Type `y` and press Enter. + +``` +Tailscale hostname [auto-detected-hostname]: +``` + +It should auto-detect. Press Enter to accept, or type your hostname. + +``` +How do you want to handle HTTPS? + 1) Use 'tailscale serve' (automatic HTTPS, recommended) + 2) Generate self-signed certificates + 3) Skip SSL setup + +Choose option (1-3) [1]: +``` + +Type `1` for automatic HTTPS (recommended). + +### Option B: After Installation + +If you already installed Chronicle, you can add Tailscale configuration: + +**Edit your environment file:** +```bash +nano environments/dev.env +``` + +**Add these lines:** +```bash +TAILSCALE_HOSTNAME=your-computer.tail12345.ts.net +HTTPS_ENABLED=true +``` + +**Save and restart:** +```bash +./start-env.sh dev +``` + +--- + +## Step 7: Set Up `tailscale serve` (HTTPS) + +Tailscale can automatically provide HTTPS for your Chronicle instance. + +### 7.1 Expose Chronicle Backend + +After Chronicle is running, run: + +```bash +sudo tailscale serve https / http://localhost:8000 +``` + +This maps: +- `https://your-computer.tail12345.ts.net/` โ†’ `http://localhost:8000` + +### 7.2 Expose Chronicle Web UI + +For the web dashboard: + +```bash +sudo tailscale serve --bg https:443 / http://localhost:3010 +``` + +Or use a different port for frontend: + +```bash +sudo tailscale serve https:8443 / http://localhost:3010 +``` + +### 7.3 Check What's Served + +```bash +tailscale serve status +``` + +You should see: +``` +https://your-computer.tail12345.ts.net (tailnet only) +|-- / proxy http://127.0.0.1:8000 +``` + +--- + +## Step 8: Test Remote Access + +### 8.1 Test from Your Phone + +1. Make sure Tailscale app is running on phone +2. Make sure you're connected (green status) +3. Open web browser on phone +4. Go to: `https://your-computer.tail12345.ts.net` + +You should see Chronicle dashboard! ๐ŸŽ‰ + +### 8.2 Test API Access + +From your phone browser, try: +`https://your-computer.tail12345.ts.net/health` + +You should see: +```json +{"status": "healthy"} +``` + +--- + +## Managing Tailscale + +### Check Connection Status + +```bash +tailscale status +``` + +Shows all connected devices. + +### Disconnect + +```bash +sudo tailscale down +``` + +### Reconnect + +```bash +sudo tailscale up +``` + +### View Logs + +```bash +tailscale status --json +``` + +### Remove Device + +1. Go to: https://login.tailscale.com/admin/machines +2. Click device name +3. Click "..." menu +4. Click "Remove device" + +--- + +## Security Best Practices + +### 1. Enable Key Expiry + +By default, device keys don't expire. Enable expiry for better security: + +1. Go to: https://login.tailscale.com/admin/settings/keys +2. Set "Key expiry" to 90 or 180 days +3. You'll need to re-authenticate periodically + +### 2. Use Tailscale ACLs + +Control which devices can access which services: + +1. Go to: https://login.tailscale.com/admin/acls +2. Edit policy to restrict access +3. Example: Only allow your phone to access Chronicle + +```json +{ + "acls": [ + { + "action": "accept", + "src": ["tag:phone"], + "dst": ["tag:server:8000,3010"] + } + ] +} +``` + +### 3. Don't Share Your Hostname Publicly + +Your Tailscale hostname is private - only devices on your Tailnet can access it. + +โŒ **Don't**: Post your `.ts.net` hostname on social media +โœ… **Do**: Share it only with trusted devices you own + +### 4. Keep Tailscale Updated + +```bash +# Update on Linux +sudo apt-get update && sudo apt-get upgrade tailscale + +# Update on macOS +brew upgrade tailscale + +# Windows/Mobile: Update from app store +``` + +--- + +## Troubleshooting + +### Can't connect from phone + +**Most common issue:** Not logged into the same Tailscale account on both devices! + +**Check Tailscale status on both devices:** + +**On your computer:** +```bash +tailscale status +# Should show your phone in the list +``` + +**On your phone:** +- Open Tailscale app +- Check for green "Connected" status +- Verify it shows your computer in the device list + +**If devices don't see each other:** +1. Make sure BOTH devices are logged into the **SAME** Tailscale account +2. Check that Tailscale is actually running on both devices +3. Try logging out and back in on both devices +4. Wait 30 seconds for devices to discover each other + +### "tailscale serve" not working + +**Check if serve is configured:** +```bash +tailscale serve status +``` + +**Restart tailscale serve:** +```bash +sudo tailscale serve reset +sudo tailscale serve https / http://localhost:8000 +``` + +### Connection works locally but not remotely + +**Firewall might be blocking:** +- Check if chronicle services are running +- Verify ports 8000 and 3010 are accessible locally +- Check Docker containers are running: `docker ps` + +**Tailscale might need restart:** +```bash +sudo tailscale down +sudo tailscale up +``` + +### "Certificate error" or "Not secure" + +**Use `tailscale serve` for automatic HTTPS:** +```bash +sudo tailscale serve https / http://localhost:8000 +``` + +This provides automatic valid SSL certificates. + +### Hostname not resolving + +**Check DNS configuration:** +```bash +tailscale status +``` + +Look for your computer's DNS name (ends in `.ts.net`). + +**Try using IP address instead:** +```bash +tailscale status # Shows 100.x.x.x IP +``` + +Use `http://100.x.x.x:8000` instead of hostname. + +--- + +## Advanced Configuration + +### Custom Port Mapping + +Serve on different ports: + +```bash +# Backend on default HTTPS (443) +sudo tailscale serve https / http://localhost:8000 + +# Frontend on port 8443 +sudo tailscale serve https:8443 / http://localhost:3010 +``` + +Access: +- Backend: `https://your-computer.tail12345.ts.net` +- Frontend: `https://your-computer.tail12345.ts.net:8443` + +### Multiple Services + +Serve multiple Chronicle environments: + +```bash +# Dev environment +sudo tailscale serve https:8000 / http://localhost:8000 + +# Staging environment +sudo tailscale serve https:8100 / http://localhost:8100 +``` + +### Tailscale SSH + +Access your computer's terminal remotely: + +```bash +# Enable SSH on server +sudo tailscale up --ssh + +# Connect from another device +ssh your-computer.tail12345.ts.net +``` + +### Exit Nodes + +Route all internet traffic through your home computer: + +```bash +# On home computer +sudo tailscale up --advertise-exit-node + +# On phone/laptop +# Tailscale app โ†’ Use exit node โ†’ Select home computer +``` + +--- + +## Cost & Limits + +### Free Tier (Personal) +- โœ… Up to **100 devices** +- โœ… Up to **3 users** +- โœ… Unlimited data transfer +- โœ… All core features +- โœ… Perfect for Chronicle personal use + +### Paid Tiers +Only needed for: +- More than 100 devices +- Business/team use +- Advanced ACLs +- Custom DNS + +**For Chronicle personal use, free tier is more than enough!** + +--- + +## Alternative: Tailscale Funnel + +**Tailscale Funnel** allows public internet access (not just your Tailnet). + +โš ๏ธ **Not recommended for Chronicle** - security risk! + +But if you need it: + +```bash +sudo tailscale funnel https / http://localhost:8000 +``` + +This exposes your Chronicle to the **entire internet**. Use with caution! + +--- + +## Summary + +โœ… **What you installed:** +1. **Tailscale on your computer** - Where Chronicle runs +2. **Tailscale on your phone** - To access Chronicle remotely +3. **Both logged into the same Tailscale account** - Required for devices to see each other + +โœ… **What you configured:** +- Chronicle knows your Tailscale hostname +- `tailscale serve` provides automatic HTTPS +- Both devices can see each other on your private network + +โœ… **What you can do now:** +- Access Chronicle from anywhere in the world +- Secure HTTPS connections automatically +- No port forwarding or firewall config needed +- Connect OMI device from anywhere with internet + +โœ… **Security:** +- End-to-end encrypted between your devices +- Only devices on YOUR Tailscale account can access +- No public exposure to the internet +- Free for personal use (up to 100 devices) + +**Remember:** Keep the Tailscale app running on both your computer and phone for remote access to work! + +--- + +## Next Steps + +1. **Test from multiple devices** - Phone, tablet, laptop +2. **Configure Chronicle mobile app** - Use your Tailscale hostname +3. **Set up OMI device** - Connect via Tailscale URL +4. **Explore Tailscale features** - SSH, exit nodes, etc. + +--- + +## Getting Help + +- **Tailscale Docs**: https://tailscale.com/kb/ +- **Tailscale Forum**: https://forum.tailscale.com/ +- **Chronicle Issues**: https://github.com/BasedHardware/Friend/issues + +--- + +## Comparison: Local vs Tailscale Access + +| Feature | Local Only | With Tailscale | +|---------|-----------|----------------| +| **Access from home WiFi** | โœ… | โœ… | +| **Access when away** | โŒ | โœ… | +| **Phone access (cellular)** | โŒ | โœ… | +| **HTTPS** | Manual setup | Automatic | +| **Port forwarding needed** | Yes (for remote) | No | +| **Firewall config** | Yes (for remote) | No | +| **Security** | Need to manage | Handled by Tailscale | +| **Setup complexity** | High (for remote) | Low | + +**Verdict**: Tailscale is highly recommended for Chronicle! ๐Ÿš€ + +Welcome to secure remote access! ๐ŸŽ‰ diff --git a/Docs/setup/windows-gitbash.md b/Docs/setup/windows-gitbash.md new file mode 100644 index 00000000..3387539c --- /dev/null +++ b/Docs/setup/windows-gitbash.md @@ -0,0 +1,714 @@ +# Chronicle Setup Guide - Windows with Git Bash + +**Installation guide for Windows using Git Bash (no WSL2 required).** + +This approach is **simpler** but has **some limitations** compared to WSL2. Good for quick testing or if WSL2 isn't available. + +--- + +## When to Use Git Bash vs WSL2 + +### Use Git Bash if: +โœ… You want the **easiest, fastest setup** +โœ… You're just **testing** Chronicle +โœ… WSL2 is **blocked** by IT policy +โœ… You prefer **Windows-native** tools +โœ… You want **minimal** learning curve + +### Use WSL2 if: +๐Ÿš€ You're a **developer** or power user +๐Ÿš€ You want **best performance** +๐Ÿš€ You need **100% script compatibility** +๐Ÿš€ You plan to use Chronicle **long-term** + +**For WSL2 setup**, see: [Windows with WSL2 Guide](windows-wsl2.md) + +--- + +## Prerequisites + +Before starting, make sure you have: +- โœ… Read [Prerequisites Guide](prerequisites.md) and have your API keys ready +- โœ… **Windows 10** or **Windows 11** +- โœ… **Administrator access** to your computer +- โœ… **At least 20GB free disk space** +- โœ… **8GB RAM** (4GB minimum) + +--- + +## Installation Steps + +### Step 1: Install Git for Windows + +Git for Windows includes Git Bash - a bash shell for Windows. + +#### 1.1 Download Git for Windows + +1. Open your web browser +2. Go to: https://git-scm.com/download/win +3. Download should start automatically (~50MB) +4. Wait for download to complete + +#### 1.2 Install Git for Windows + +1. Open Downloads folder +2. Double-click `Git--64-bit.exe` +3. Click "Yes" when asked to allow changes + +**Installation wizard settings:** + +**Select Components:** +- โœ… Windows Explorer integration +- โœ… Git Bash Here +- โœ… Git GUI Here +- โœ… Associate .git* configuration files +- โœ… Associate .sh files to be run with Bash + +**Adjusting PATH:** +- Select: **"Git from the command line and also from 3rd-party software"** (recommended) + +**SSH executable:** +- Select: **"Use bundled OpenSSH"** + +**HTTPS backend:** +- Select: **"Use the OpenSSL library"** + +**Line ending conversions:** +- โš ๏ธ **IMPORTANT**: Select **"Checkout as-is, commit as-is"** + - This prevents line ending issues with bash scripts + +**Terminal emulator:** +- Select: **"Use MinTTY"** (better terminal) + +**Default branch name:** +- Select: **"Let Git decide"** or "main" + +**Other settings:** +- Keep all other defaults + +Click "Install" and wait (~2 minutes). + +Click "Finish" when done. + +โœ… **Git Bash is installed!** + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 2: Install Docker Desktop + +Docker Desktop runs Chronicle's containers. + +#### 2.1 Download Docker Desktop + +1. Go to: https://www.docker.com/products/docker-desktop +2. Click "Download for Windows" +3. Wait for download (~500MB) + +#### 2.2 Install Docker Desktop + +1. Open Downloads folder +2. Double-click `Docker Desktop Installer.exe` +3. Click "Yes" when asked to allow changes + +**Important settings:** +- The installer will detect if WSL2 is available +- If prompted, choose: **"Use Hyper-V"** (not WSL2, since we're not using WSL2) +- โœ… "Add shortcut to desktop" (optional) + +Click "OK" to install (5-10 minutes). + +When finished: Click "Close and restart" + +**Your computer will restart.** + +#### 2.3 Start Docker Desktop + +After restart: + +1. Docker Desktop should start automatically (whale icon in system tray) +2. If not: Start โ†’ type "Docker Desktop" โ†’ press Enter + +**First-time setup:** +1. Accept "Docker Subscription Service Agreement" (free for personal use) +2. Skip tutorial or close welcome window + +#### 2.4 Wait for Docker to Start + +Look at the Docker Desktop icon in system tray: +- ๐ŸŸข **Solid whale** = Docker is running (good!) +- ๐ŸŸ  **Animated whale** = Docker is starting (wait) +- ๐Ÿ”ด **Whale with X** = Docker has a problem (see troubleshooting) + +**Wait until the whale icon is solid** (can take 2-3 minutes first time). + +#### 2.5 Verify Docker Works + +1. Right-click Start button +2. Click "Windows PowerShell" (or "Terminal") +3. Type: + ```powershell + docker --version + ``` +4. Press Enter + +**You should see:** +``` +Docker version 24.x.x, build xxxxxxx +``` + +โœ… **Docker is working!** + +โฑ๏ธ **Time: 15 minutes** + +--- + +### Step 3: Install Chronicle Dependencies + +Git for Windows includes most tools we need, but let's verify. + +#### 3.1 Open Git Bash + +**Method 1:** +1. Click Start +2. Type "git bash" +3. Press Enter + +**Method 2:** +1. Right-click on Desktop or in any folder +2. Click "Git Bash Here" + +You should see a terminal window with: +``` +yourname@DESKTOP-XXXXX MINGW64 ~ +$ +``` + +#### 3.2 Verify Tools + +**Test each tool:** + +```bash +git --version +``` +Should show: `git version 2.x.x` + +```bash +make --version +``` +Should show: `GNU Make 4.x` or error (we'll fix if missing) + +```bash +docker --version +``` +Should show: `Docker version 24.x.x` + +```bash +docker compose version +``` +Should show: `Docker Compose version v2.x.x` + +#### 3.3 Install Make (if missing) + +If `make --version` gave an error, install it: + +**In Git Bash:** +```bash +# Download and install make +curl -L https://github.com/mstorsjo/llvm-mingw/releases/download/20231128/llvm-mingw-20231128-ucrt-x86_64.zip -o /tmp/mingw.zip +unzip /tmp/mingw.zip -d /tmp/ +cp /tmp/llvm-mingw*/bin/mingw32-make.exe /usr/bin/make.exe +``` + +Then verify: +```bash +make --version +``` + +โœ… **All tools verified!** + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 4: Install Tailscale (Optional but Recommended) + +Tailscale lets you access Chronicle remotely from your phone or other devices. + +**Skip this if you only want local access.** + +#### 4.1 Download Tailscale for Windows + +1. Go to: https://tailscale.com/download/windows +2. Click "Download Tailscale for Windows" +3. Wait for download (~20MB) + +#### 4.2 Install Tailscale + +1. Open Downloads folder +2. Double-click `tailscale-setup-.exe` +3. Click "Yes" when asked +4. Click "Install" +5. Wait for installation (~1 minute) +6. Click "Finish" + +#### 4.3 Connect to Tailscale + +1. Tailscale icon appears in system tray (near clock) +2. Click the Tailscale icon +3. Click "Log in" +4. Browser opens - log in with: + - Google account + - Microsoft account + - Email (create new account) +5. After logging in, close browser +6. Tailscale icon should turn green โœ… + +#### 4.4 Get Your Tailscale Hostname + +1. Click Tailscale icon in system tray +2. Your hostname is shown (e.g., `my-computer.tail12345.ts.net`) +3. **Write this down** - you'll need it in setup + +For detailed instructions, see: **[Tailscale Setup Guide](tailscale.md)** + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 5: Install Chronicle + +Now for the main installation! + +#### 5.1 Choose Installation Directory + +**In Git Bash, decide where to install:** + +**Option A: In your home directory (recommended)** +```bash +cd ~ +``` + +**Option B: In a specific folder** +```bash +cd /c/Users/YourUsername/Projects +``` + +**โš ๏ธ Important**: Avoid paths with spaces! +- โœ… Good: `/c/Users/John/code` +- โŒ Bad: `/c/Users/John Smith/My Projects` + +#### 5.2 Clone Chronicle Repository + +```bash +git clone https://github.com/BasedHardware/Friend.git chronicle +cd chronicle +``` + +**What this does:** +- Downloads Chronicle code +- Creates `chronicle` folder +- Enters the folder + +You should see: +``` +yourname@DESKTOP-XXXXX MINGW64 ~/chronicle (main) +$ +``` + +#### 5.3 Run the Setup Wizard + +```bash +make wizard +``` + +Press `Enter`. + +**You'll see:** +``` +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +๐Ÿง™ Chronicle Setup Wizard +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +``` + +Press `Enter` to continue. + +#### 5.4 Configure Secrets + +The wizard asks several questions. **Have your API keys ready from the [Prerequisites Guide](prerequisites.md).** + +**Enter when prompted:** +- **JWT Secret Key**: Press Enter (auto-generates) +- **Admin Email**: Press Enter (use default) or type your email +- **Admin Password**: Type a secure password +- **OpenAI API Key**: Paste your key (Right-click or Shift+Insert to paste in Git Bash) +- **Deepgram API Key**: Paste your key +- **Optional keys**: Press Enter to skip (Mistral, Hugging Face, etc.) + +#### 5.5 Tailscale Configuration + +``` +Do you want to configure Tailscale? (y/N): +``` + +- If you installed Tailscale in Step 4: Type `y` +- If you skipped Tailscale: Type `N` + +**If you said yes:** +``` +Tailscale hostname [auto-detected]: +``` +๐Ÿ‘‰ Press Enter (should auto-detect) + +**SSL Options:** +``` +1) Use 'tailscale serve' (automatic HTTPS, recommended) +2) Generate self-signed certificates +3) Skip SSL setup + +Choose option (1-3) [1]: +``` +๐Ÿ‘‰ Type `1` and press Enter + +#### 5.6 Environment Setup + +``` +Environment name [dev]: +``` +๐Ÿ‘‰ Press Enter + +``` +Port offset [0]: +``` +๐Ÿ‘‰ Press Enter + +``` +MongoDB database name [chronicle-dev]: +``` +๐Ÿ‘‰ Press Enter + +``` +Mycelia database name [mycelia-dev]: +``` +๐Ÿ‘‰ Press Enter + +**Optional services - type N for all:** +- Enable Mycelia?: `N` +- Enable Speaker Recognition?: `N` +- Enable OpenMemory MCP?: `N` +- Enable Parakeet ASR?: `N` + +#### 5.7 Wizard Complete + +``` +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +โœ… Setup Complete! +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +๐Ÿš€ Next Steps: + + Start your environment: + ./start-env.sh dev +``` + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 6: Start Chronicle + +#### 6.1 Start the Services + +**In Git Bash:** + +```bash +./start-env.sh dev +``` + +Press `Enter`. + +**What happens:** + +1. Docker downloads images (first time - ~5 minutes) +2. Docker builds services +3. Services start + +**You'll see:** +``` +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +๐Ÿš€ Starting Chronicle: dev +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +[+] Building 123.4s (15/15) FINISHED +[+] Running 5/5 + โœ” Container chronicle-dev-mongo-1 Started + โœ” Container chronicle-dev-redis-1 Started + โœ” Container chronicle-dev-qdrant-1 Started + โœ” Container chronicle-dev-friend-backend-1 Started + โœ” Container chronicle-dev-webui-1 Started + +โœ… Services Started Successfully! +``` + +**First-time startup: 5-10 minutes** +**Subsequent starts: ~30 seconds** + +#### 6.2 Verify in Docker Desktop + +1. Open Docker Desktop +2. Click "Containers" +3. You should see 5 containers with green "Running" status + +โฑ๏ธ **Time: 10 minutes (first time)** + +--- + +### Step 7: Access Chronicle + +#### 7.1 Open the Web Dashboard + +1. Open your web browser +2. Go to: **http://localhost:3010** + +You should see the Chronicle login page! + +#### 7.2 Log In + +**Enter your credentials:** +- Email: `admin@example.com` (or what you chose) +- Password: (your admin password) + +Click "Sign In" + +๐ŸŽ‰ **You're in!** Welcome to Chronicle! + +#### 7.3 Explore + +You should see: +- ๐Ÿ“Š Dashboard +- ๐Ÿ’ฌ Conversations +- ๐Ÿง  Memories +- โš™๏ธ Settings + +**Check API docs:** +- Go to: **http://localhost:8000/docs** + +โฑ๏ธ **Time: 2 minutes** + +--- + +## Managing Chronicle + +### Starting Chronicle (after stopping) + +```bash +# In Git Bash +cd ~/chronicle # or wherever you installed +./start-env.sh dev +``` + +### Stopping Chronicle + +```bash +cd ~/chronicle +docker compose down +``` + +**Or use Docker Desktop:** +1. Click "Containers" +2. Click stop button + +### Viewing Logs + +**In Docker Desktop:** +1. Click "Containers" +2. Click container name +3. Click "Logs" + +**Or in Git Bash:** +```bash +cd ~/chronicle +docker compose logs -f +``` + +### Restarting Services + +```bash +cd ~/chronicle +docker compose restart +``` + +### Updating Chronicle + +```bash +cd ~/chronicle +git pull +docker compose up -d --build +``` + +--- + +## Limitations of Git Bash Setup + +### Known Issues + +โŒ **Some bash scripts may not work perfectly** +- Git Bash is not full Linux +- Some Unix tools are limited or missing +- Path conversions can cause issues + +โŒ **Performance slightly slower** +- Docker Desktop uses Hyper-V virtualization +- Not as fast as native Linux (WSL2) + +โŒ **File permissions can be tricky** +- Windows vs Linux file permissions differ +- May see warnings about file modes + +### Workarounds + +**Script doesn't work?** +Try running in PowerShell or modify for Windows: +```powershell +# PowerShell equivalent commands often available +``` + +**Path issues?** +Use forward slashes: `/c/Users/...` not `C:\Users\...` + +**Line ending errors?** +Configure Git: +```bash +git config --global core.autocrlf false +``` + +--- + +## When to Switch to WSL2 + +Consider switching to [WSL2 setup](windows-wsl2.md) if: +- ๐Ÿš€ You use Chronicle **regularly** +- ๐Ÿ› You encounter **script compatibility issues** +- โšก You want **better performance** +- ๐Ÿ‘จโ€๐Ÿ’ป You're doing **development** work + +Switching is easy - your API keys and settings can be reused! + +--- + +## Common Issues + +### "docker: command not found" + +**Solution:** +1. Make sure Docker Desktop is running +2. Restart Git Bash +3. Verify: `docker --version` + +### "Permission denied" when starting services + +**Solution - Run Git Bash as Administrator:** +1. Right-click "Git Bash" +2. Click "Run as administrator" + +### Port conflicts (8000, 3010 in use) + +**Solution - Use different ports:** +Edit `environments/dev.env`: +```bash +PORT_OFFSET=100 +``` + +### Scripts fail with "bad interpreter" + +**Solution - Check line endings:** +```bash +cd ~/chronicle +git config core.autocrlf false +git checkout -- . +``` + +### "make: command not found" + +**Solution - Install make:** +Follow Step 3.3 above to install make manually. + +--- + +## Next Steps + +Now that Chronicle is running: + +1. **Connect a device** - Set up mobile app or OMI device +2. **Configure settings** - Check Settings โ†’ Memory Provider +3. **Test with audio** - Upload a test audio file +4. **Read docs** - Check `CLAUDE.md` for full guide +5. **Explore API** - Visit http://localhost:8000/docs + +--- + +## Advanced Tips + +### Better Terminal + +**Windows Terminal** is better than Git Bash: +1. Microsoft Store โ†’ "Windows Terminal" +2. Install it +3. Open new "Git Bash" tab + +### IDE Integration + +**VS Code works great with Git Bash:** +1. Install VS Code +2. Open folder: `File โ†’ Open Folder โ†’ chronicle` +3. Terminal automatically uses Git Bash + +### PATH Configuration + +Add Git Bash tools to Windows PATH for PowerShell access: +1. Start โ†’ "Environment Variables" +2. Edit PATH +3. Add: `C:\Program Files\Git\usr\bin` + +--- + +## Getting Help + +- **GitHub Issues**: https://github.com/BasedHardware/Friend/issues +- **Git Bash Docs**: https://git-scm.com/doc +- **Docker Docs**: https://docs.docker.com/desktop/windows/ + +--- + +## Summary + +โœ… **What you installed:** +- Git for Windows (includes Git Bash) +- Docker Desktop for Windows +- Chronicle and all dependencies +- (Optional) Tailscale + +โœ… **What you can do:** +- Access dashboard: http://localhost:3010 +- Access API: http://localhost:8000 +- Manage containers via Docker Desktop GUI +- Use Windows tools natively + +โœ… **Trade-offs:** +- โœ… Easiest setup +- โœ… Windows-native experience +- โš ๏ธ Some script compatibility limitations +- โš ๏ธ Slightly slower performance than WSL2 + +**This setup is perfect for:** +- ๐Ÿงช Testing Chronicle +- ๐Ÿ“ฑ Quick installations +- ๐ŸชŸ Windows-native workflows + +**Consider upgrading to [WSL2](windows-wsl2.md) for:** +- ๐Ÿš€ Best performance +- ๐Ÿ’ฏ Full compatibility +- ๐Ÿ‘จโ€๐Ÿ’ป Serious development work + +Welcome to Chronicle! ๐ŸŽ‰ diff --git a/Docs/setup/windows-wsl2.md b/Docs/setup/windows-wsl2.md new file mode 100644 index 00000000..3c73660b --- /dev/null +++ b/Docs/setup/windows-wsl2.md @@ -0,0 +1,738 @@ +# Chronicle Setup Guide - Windows with WSL2 + +**Complete installation guide for Windows using WSL2 (Windows Subsystem for Linux).** + +This is the **recommended approach** for Windows users - it provides the best performance and compatibility. + +--- + +## Why WSL2? + +โœ… **Best performance** - Docker runs natively in Linux kernel (no VM overhead) +โœ… **Perfect compatibility** - All Chronicle bash scripts work exactly as intended +โœ… **Native Docker** - Can use Docker Desktop or native Docker engine +โœ… **Industry standard** - Most developers on Windows use WSL2 +โœ… **Future-proof** - Microsoft's recommended way to run Linux on Windows + +--- + +## Prerequisites + +Before starting, make sure you have: +- โœ… Read [Prerequisites Guide](prerequisites.md) and have your API keys ready +- โœ… **Windows 10 version 2004+** (Build 19041+) or **Windows 11** +- โœ… **Administrator access** to your computer +- โœ… **At least 20GB free disk space** +- โœ… **8GB RAM** (4GB minimum) + +**Check your Windows version:** +1. Press `Win + R` +2. Type `winver` and press Enter +3. Check the version number + +If your version is older than 2004, run Windows Update first. + +--- + +## Installation Steps + +### Step 1: Install WSL2 with Ubuntu + +WSL2 gives you a real Linux environment inside Windows. + +#### 1.1 Open PowerShell as Administrator + +**Method 1:** +1. Click Start button +2. Type `powershell` +3. Right-click "Windows PowerShell" +4. Click "Run as administrator" +5. Click "Yes" when asked + +**Method 2:** +1. Press `Win + X` +2. Click "Windows PowerShell (Admin)" or "Terminal (Admin)" + +You should see a blue window with: `PS C:\Windows\system32>` + +#### 1.2 Install WSL2 with Ubuntu + +**Copy and paste this command:** + +```powershell +wsl --install -d Ubuntu-22.04 +``` + +Press `Enter`. + +**What you'll see:** +``` +Installing: Windows Subsystem for Linux +Installing: Ubuntu-22.04 +The requested operation is successful. Changes will not be effective until the system is rebooted. +``` + +#### 1.3 Restart Your Computer + +**You MUST restart** for WSL2 to work. + +1. Close PowerShell +2. Save any open work +3. Restart Windows (Start โ†’ Power โ†’ Restart) + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 2: Set Up Ubuntu + +After restarting, Ubuntu will finish installing. + +#### 2.1 Wait for Ubuntu Setup + +1. A window titled "Ubuntu" will open automatically +2. You'll see: `Installing, this may take a few minutes...` +3. **Be patient** - this can take 5-10 minutes + +#### 2.2 Create Your Ubuntu Username + +When you see `Enter new UNIX username:`: + +1. Type a username (lowercase, no spaces) + - Example: `john` or `yourname` + - โš ๏ธ This is NOT your Windows username +2. Press `Enter` + +#### 2.3 Create Your Ubuntu Password + +When you see `New password:`: + +1. Type a password + - **The cursor won't move** - this is normal Linux security! + - Use something you'll remember +2. Press `Enter` +3. Type the same password again +4. Press `Enter` + +**โš ๏ธ Important**: Remember this password! You'll need it throughout setup. + +#### 2.4 Verify Ubuntu is Working + +You should now see: +``` +yourname@DESKTOP-XXXXX:~$ +``` + +This is your **terminal prompt** - you're now inside Linux! + +**Test it:** +```bash +pwd +``` + +Press `Enter`. You should see: +``` +/home/yourname +``` + +โœ… **Ubuntu is installed and working!** + +โฑ๏ธ **Time: 10 minutes** + +--- + +### Step 3: Install Docker Desktop + +Docker Desktop lets you run Chronicle's containers and automatically integrates with WSL2. + +#### 3.1 Download Docker Desktop + +1. Open your web browser (Edge, Chrome, Firefox) +2. Go to: https://www.docker.com/products/docker-desktop +3. Click "Download for Windows" +4. Wait for download (file is ~500MB) + +#### 3.2 Install Docker Desktop + +1. Open your Downloads folder +2. Double-click `Docker Desktop Installer.exe` +3. Click "Yes" when asked to allow changes +4. **Important checkboxes:** + - โœ… "Use WSL 2 instead of Hyper-V" (should be checked by default) + - โœ… "Add shortcut to desktop" (optional but helpful) +5. Click "OK" +6. Wait for installation (5-10 minutes) +7. When you see "Installation succeeded", click "Close and restart" + +**Your computer will restart again.** + +#### 3.3 Start Docker Desktop + +After restart: + +1. Docker Desktop should start automatically (whale icon in system tray) +2. If not: Start โ†’ type "Docker Desktop" โ†’ press Enter + +**First-time setup:** +1. Accept "Docker Subscription Service Agreement" (free for personal use) +2. Skip tutorial or close welcome window + +#### 3.4 Enable WSL2 Integration + +**This step is CRITICAL:** + +1. Click the โš™๏ธ gear icon (Settings) in Docker Desktop +2. Click "Resources" in left menu +3. Click "WSL Integration" +4. Enable: + - โœ… "Enable integration with my default WSL distro" + - โœ… Toggle for "Ubuntu-22.04" (turn ON/blue) +5. Click "Apply & Restart" +6. Wait for Docker to restart (~30 seconds) + +#### 3.5 Verify Docker Works in WSL2 + +1. Open Ubuntu (Start โ†’ type "Ubuntu" โ†’ press Enter) +2. Type: + ```bash + docker --version + ``` +3. Press Enter + +**You should see:** +``` +Docker version 24.x.x, build xxxxxxx +``` + +**Also test Docker Compose:** +```bash +docker compose version +``` + +**You should see:** +``` +Docker Compose version v2.x.x +``` + +โœ… **Docker is working in WSL2!** + +โฑ๏ธ **Time: 15 minutes** + +--- + +### Step 4: Install Chronicle Dependencies + +Now we'll install the tools Chronicle needs using an automated script. + +#### 4.1 Open Ubuntu Terminal + +1. Click Start +2. Type `ubuntu` +3. Press Enter + +You should see: `yourname@DESKTOP-XXXXX:~$` + +#### 4.2 Run the Dependency Installer + +**Copy and paste this command:** + +```bash +curl -fsSL https://raw.githubusercontent.com/BasedHardware/Friend/main/scripts/install-deps.sh | bash +``` + +Press `Enter`. + +**You'll be asked for your Ubuntu password:** +- Type your password (cursor won't move - normal!) +- Press Enter + +**What the script does:** +- Updates Ubuntu's package lists +- Installs git, make, curl, and other tools +- Detects you're in WSL2 +- Asks about Docker installation + +**Docker Installation Prompt:** + +The script will detect WSL2 and ask: +``` +Install Docker Engine in WSL? (y/N): +``` + +**Type `N` and press Enter** - you already installed Docker Desktop! + +The script will show: +``` +โ„น๏ธ Skipping Docker installation + Please install Docker Desktop for Windows, then: + 1. Open Docker Desktop Settings + 2. Go to Resources โ†’ WSL Integration + 3. Enable integration with Ubuntu +``` + +โœ… You already did this in Step 3.4! + +#### 4.3 Verify Installation + +The script automatically verifies everything: + +``` +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +โœ… Dependency Installation Complete! +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +๐Ÿ“‹ Installed tools: + + โœ… Git: 2.34.1 + โœ… Make: 4.3 + โœ… curl: 7.81.0 + โœ… Docker: 24.0.5 + โœ… Docker Compose: v2.20.2 + +โœ… Docker is running and accessible +``` + +โฑ๏ธ **Time: 3 minutes** + +--- + +### Step 5: Install Tailscale (Optional but Recommended) + +Tailscale lets you access Chronicle from anywhere - your phone, other computers, etc. + +**Skip this if you only want local access** (same WiFi network only). + +See: **[Tailscale Setup Guide](tailscale.md)** for detailed instructions. + +**Quick install:** +```bash +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up +``` + +Follow the login prompts to connect your device. + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 6: Install Chronicle + +Now for the main installation! + +#### 6.1 Clone Chronicle Repository + +**In Ubuntu terminal:** + +```bash +cd ~ +git clone https://github.com/BasedHardware/Friend.git chronicle +cd chronicle +``` + +**What this does:** +- `cd ~` - Go to your home directory +- `git clone` - Download Chronicle code +- `cd chronicle` - Enter the Chronicle folder + +You should now see: +``` +yourname@DESKTOP-XXXXX:~/chronicle$ +``` + +#### 6.2 Run the Setup Wizard + +**Copy and paste:** + +```bash +make wizard +``` + +Press `Enter`. + +You'll see: +``` +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +๐Ÿง™ Chronicle Setup Wizard +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +``` + +Press `Enter` to continue. + +#### 6.3 Configure Secrets + +The wizard will ask you several questions. **Have your API keys ready from the [Prerequisites Guide](prerequisites.md).** + +**Enter when prompted:** +- **JWT Secret Key**: Press Enter (auto-generates) +- **Admin Email**: Press Enter (use default) or type your email +- **Admin Password**: Type a secure password (cursor won't move - normal!) +- **OpenAI API Key**: Paste your key (from Prerequisites guide) +- **Deepgram API Key**: Paste your key (from Prerequisites guide) +- **Optional keys**: Press Enter to skip (Mistral, Hugging Face, etc.) + +#### 6.4 Tailscale Configuration + +``` +Do you want to configure Tailscale? (y/N): +``` + +- If you installed Tailscale in Step 5: Type `y` +- If you skipped Tailscale: Type `N` + +#### 6.5 Environment Setup + +``` +Environment name [dev]: +``` +๐Ÿ‘‰ Press Enter (use "dev") + +``` +Port offset [0]: +``` +๐Ÿ‘‰ Press Enter (use default ports) + +``` +MongoDB database name [chronicle-dev]: +``` +๐Ÿ‘‰ Press Enter + +``` +Mycelia database name [mycelia-dev]: +``` +๐Ÿ‘‰ Press Enter + +**Optional services - type N for all (can enable later):** +``` +Enable Mycelia? (y/N): +``` +๐Ÿ‘‰ Type `N` + +``` +Enable Speaker Recognition? (y/N): +``` +๐Ÿ‘‰ Type `N` + +``` +Enable OpenMemory MCP? (y/N): +``` +๐Ÿ‘‰ Type `N` + +``` +Enable Parakeet ASR? (y/N): +``` +๐Ÿ‘‰ Type `N` + +#### 6.6 Wizard Complete + +You should see: +``` +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +โœ… Setup Complete! +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +๐Ÿš€ Next Steps: + + Start your environment: + ./start-env.sh dev +``` + +โฑ๏ธ **Time: 5 minutes** + +--- + +### Step 7: Start Chronicle + +#### 7.1 Start the Services + +**In Ubuntu terminal:** + +```bash +./start-env.sh dev +``` + +Press `Enter`. + +**What happens:** + +1. Docker downloads container images (first time only - ~5 minutes) +2. Docker builds the Chronicle services +3. Services start in the background + +**You'll see:** +``` +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +๐Ÿš€ Starting Chronicle: dev +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +๐Ÿ“ฆ Project: chronicle-dev +๐Ÿ—„๏ธ MongoDB Database: chronicle-dev +๐Ÿ’พ Data Directory: ./data/dev + +๐ŸŒ Service URLs: + Backend: http://localhost:8000 + Web UI: http://localhost:3010 + MongoDB: mongodb://localhost:27017 +... + +[+] Building 123.4s (15/15) FINISHED +[+] Running 5/5 + โœ” Container chronicle-dev-mongo-1 Started + โœ” Container chronicle-dev-redis-1 Started + โœ” Container chronicle-dev-qdrant-1 Started + โœ” Container chronicle-dev-friend-backend-1 Started + โœ” Container chronicle-dev-webui-1 Started + +โœ… Services Started Successfully! +``` + +**First-time startup takes 5-10 minutes.** Subsequent starts are much faster (~30 seconds). + +#### 7.2 Verify Services + +**Open Docker Desktop:** +1. Click the whale icon in system tray +2. Click "Containers" +3. You should see 5 containers running with green status + +โฑ๏ธ **Time: 10 minutes (first time)** + +--- + +### Step 8: Access Chronicle + +#### 8.1 Open the Web Dashboard + +1. Open your web browser (Chrome, Edge, Firefox) +2. Go to: **http://localhost:3010** + +You should see the Chronicle login page! + +#### 8.2 Log In + +**Enter your credentials:** +- Email: `admin@example.com` (or what you chose) +- Password: (your admin password from Step 6.3) + +Click "Sign In" + +๐ŸŽ‰ **You're in!** Welcome to Chronicle! + +#### 8.3 Explore + +You should see: +- ๐Ÿ“Š Dashboard with stats +- ๐Ÿ’ฌ Conversations tab +- ๐Ÿง  Memories tab +- โš™๏ธ Settings + +**Check the API docs:** +- Go to: **http://localhost:8000/docs** +- This shows all available API endpoints + +โฑ๏ธ **Time: 2 minutes** + +--- + +## Managing Chronicle + +### Starting Chronicle (after stopping) + +```bash +# Open Ubuntu terminal +cd ~/chronicle +./start-env.sh dev +``` + +### Stopping Chronicle + +```bash +cd ~/chronicle +docker compose down +``` + +**Or use Docker Desktop:** +1. Open Docker Desktop +2. Click "Containers" +3. Click stop button next to `chronicle-dev` + +### Viewing Logs + +**In Docker Desktop:** +1. Click "Containers" +2. Click container name +3. Click "Logs" tab + +**Or in terminal:** +```bash +cd ~/chronicle +docker compose logs -f +``` + +Press `Ctrl+C` to stop following logs. + +### Restarting Services + +```bash +cd ~/chronicle +docker compose restart +``` + +### Updating Chronicle + +```bash +cd ~/chronicle +git pull +docker compose up -d --build +``` + +--- + +## Accessing WSL2 Files from Windows + +Your Chronicle files live in Ubuntu (WSL2), but you can access them from Windows: + +**In Windows File Explorer:** +1. Open File Explorer +2. Type in address bar: `\\wsl$\Ubuntu-22.04\home\yourname\chronicle` +3. Press Enter + +You can now browse Chronicle files like normal Windows folders! + +**Or access from Ubuntu:** +- Windows `C:\` drive is at: `/mnt/c/` +- Your Windows user folder: `/mnt/c/Users/YourWindowsUsername/` + +--- + +## Common Issues + +### "wsl --install" doesn't work + +**Solution - Enable WSL manually:** +```powershell +# In PowerShell as Administrator +dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart +dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart + +# Restart computer, then: +wsl --set-default-version 2 +wsl --install -d Ubuntu-22.04 +``` + +### Docker Desktop says "WSL 2 installation is incomplete" + +**Solution:** +1. Download WSL2 kernel update: https://aka.ms/wsl2kernel +2. Install it +3. Restart Docker Desktop + +### "Cannot connect to Docker daemon" + +**Solution:** +1. Make sure Docker Desktop is running (whale icon in system tray should be present) +2. In Docker Desktop Settings โ†’ Resources โ†’ WSL Integration +3. Enable "Ubuntu-22.04" +4. Click "Apply & Restart" + +### Port 8000 or 3010 already in use + +**Solution - Find and kill the process:** +```powershell +# In PowerShell as Administrator +netstat -ano | findstr :8000 +taskkill /PID /F +``` + +**Or use different ports:** +Edit `environments/dev.env` and change `PORT_OFFSET=100` + +### Ubuntu terminal closes immediately + +**Solution - Reinstall Ubuntu:** +```powershell +# In PowerShell as Administrator +wsl --unregister Ubuntu-22.04 +wsl --install -d Ubuntu-22.04 +``` +Then go through Ubuntu setup again. + +### "Permission denied" errors + +**Solution:** +```bash +# Make sure you own the chronicle folder +sudo chown -R $USER:$USER ~/chronicle +cd ~/chronicle +``` + +--- + +## Next Steps + +Now that Chronicle is running: + +1. **Connect a device** - Set up the mobile app or OMI device +2. **Configure settings** - Check Settings โ†’ Memory Provider +3. **Test audio upload** - Try uploading a test audio file +4. **Read the docs** - Check `CLAUDE.md` for comprehensive guide +5. **Explore the API** - Visit http://localhost:8000/docs + +--- + +## Advanced Tips + +### Using VS Code with WSL2 + +VS Code can edit files directly in WSL2: + +1. Install "Remote - WSL" extension in VS Code +2. In Ubuntu terminal: `code ~/chronicle` +3. VS Code opens with WSL2 integration! + +### Better Terminal + +Install Windows Terminal for better experience: +1. Microsoft Store โ†’ search "Windows Terminal" +2. Install and set as default +3. Opens Ubuntu tabs easily + +### Docker Desktop Alternatives + +You can skip Docker Desktop and use native Docker in WSL2: +- Lighter weight (no GUI) +- Completely free (no licensing) +- Runs `./scripts/install-deps.sh` and choose "y" to install Docker Engine + +--- + +## Getting Help + +- **GitHub Issues**: https://github.com/BasedHardware/Friend/issues +- **WSL2 Docs**: https://docs.microsoft.com/en-us/windows/wsl/ +- **Docker Docs**: https://docs.docker.com/desktop/windows/ + +--- + +## Summary + +โœ… **What you installed:** +- WSL2 with Ubuntu 22.04 +- Docker Desktop with WSL2 backend +- Chronicle and all dependencies +- (Optional) Tailscale + +โœ… **What you can do:** +- Access dashboard: http://localhost:3010 +- Access API: http://localhost:8000 +- Manage containers via Docker Desktop GUI +- Edit files from Windows File Explorer +- Everything "just works" together! + +**Your WSL2 setup gives you:** +- ๐Ÿš€ Best performance (native Linux kernel) +- ๐Ÿ”ง Perfect compatibility (all scripts work) +- ๐ŸชŸ Windows integration (GUI tools available) +- ๐Ÿ’ช Professional dev environment + +Welcome to Chronicle! ๐ŸŽ‰ diff --git a/backends/advanced/compose/infrastructure.yml b/backends/advanced/compose/infrastructure.yml deleted file mode 100644 index 5d45474a..00000000 --- a/backends/advanced/compose/infrastructure.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Infrastructure Services -# Core database and cache services required by all environments - -services: - qdrant: - image: qdrant/qdrant:latest - ports: - - "${QDRANT_GRPC_PORT:-6033}:6333" - - "${QDRANT_HTTP_PORT:-6034}:6334" - volumes: - - ${QDRANT_DATA_PATH:-../data/qdrant_data}:/qdrant/storage - networks: - - infra-network - - chronicle-network - - mongo: - image: mongo:8.0.14 - ports: - - "${MONGO_PORT:-27017}:27017" - volumes: - - mongo_data:/data/db - healthcheck: - test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand({ ping: 1 })"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - networks: - - infra-network - - chronicle-network - - redis: - image: redis:7-alpine - ports: - - "${REDIS_PORT:-6379}:6379" - volumes: - - ${REDIS_DATA_PATH:-../data/redis_data}:/data - command: redis-server --appendonly yes - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 5 - networks: - - infra-network - - chronicle-network - -networks: - infra-network: - driver: bridge - chronicle-network: - name: chronicle-network - external: true - -volumes: - mongo_data: - driver: local diff --git a/backends/advanced/compose/mycelia.yml b/backends/advanced/compose/mycelia.yml deleted file mode 100644 index eec877ba..00000000 --- a/backends/advanced/compose/mycelia.yml +++ /dev/null @@ -1,9 +0,0 @@ -# Mycelia Services (Backend-Level Reference) -# NOTE: Mycelia is now defined at project root level (../../compose/mycelia.yml) -# This file kept for backward compatibility when running from backends/advanced/ -# -# Mycelia is at the same level as other extras (openmemory, speaker, asr) -# Use from project root: docker compose --profile mycelia up - -# Empty services - mycelia defined at root level -services: {} diff --git a/compose/advanced-backend.yml b/compose/advanced-backend.yml deleted file mode 100644 index 6ab98543..00000000 --- a/compose/advanced-backend.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Friend-Lite Advanced Backend -# Main backend services - API, workers, databases, web UI -# This is the core Friend-Lite stack - -include: - - path: ../backends/advanced/docker-compose.yml - env_file: ../backends/advanced/.env From 089dc48fad126a52b9b37a66e76a48ff8394acf2 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Tue, 9 Dec 2025 22:05:04 +0000 Subject: [PATCH 14/21] added installer --- INSTALL.md | 250 +++++++ Makefile | 626 +++++++----------- README.md | 1 + backends/advanced/compose/backend.yml | 20 +- backends/advanced/compose/frontend.yml | 3 +- .../advanced/compose/optional-services.yml | 26 +- compose/asr-services.yml | 4 +- compose/caddy.yml | 43 ++ compose/infrastructure-shared.yml | 128 ++++ compose/mycelia.yml | 9 - compose/openmemory.yml | 30 +- compose/speaker-recognition.yml | 6 + docker-compose.yml | 5 +- scripts/configure-tailscale-serve.sh | 131 ++++ scripts/finalize-setup.sh | 94 +++ scripts/generate-caddyfile.sh | 183 +++++ scripts/install-deps.sh | 318 +++++++++ scripts/setup-environment.sh | 286 ++++++++ scripts/setup-secrets.sh | 136 ++++ scripts/setup-tailscale.sh | 295 +++++++++ start-env.sh | 595 ++++++++++++++++- 21 files changed, 2727 insertions(+), 462 deletions(-) create mode 100644 INSTALL.md create mode 100644 compose/caddy.yml create mode 100644 compose/infrastructure-shared.yml create mode 100755 scripts/configure-tailscale-serve.sh create mode 100755 scripts/finalize-setup.sh create mode 100755 scripts/generate-caddyfile.sh create mode 100755 scripts/install-deps.sh create mode 100755 scripts/setup-environment.sh create mode 100755 scripts/setup-secrets.sh create mode 100755 scripts/setup-tailscale.sh diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 00000000..7ebcccf5 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,250 @@ +# Chronicle Installation Guide + +Choose your operating system to get started with Chronicle: + +## ๐ŸชŸ Windows + +**Complete step-by-step guide for Windows users (including WSL2 setup)** + +๐Ÿ‘‰ **[Windows Setup Guide](WINDOWS-SETUP.md)** + +- Fresh Windows install instructions +- Automated dependency installation +- Docker Desktop + WSL2 setup +- Everything explained in detail + +**Quick Install (if you already have WSL2):** +```bash +# In WSL2 Ubuntu terminal +curl -fsSL https://raw.githubusercontent.com/BasedHardware/Friend/main/scripts/install-deps.sh | bash +cd ~ +git clone https://github.com/BasedHardware/Friend.git chronicle +cd chronicle +make wizard +``` + +--- + +## ๐ŸŽ macOS + +**Installation for Mac users** + +### Prerequisites + +Install Homebrew (if not already installed): +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +### Quick Install + +```bash +# Install dependencies automatically +curl -fsSL https://raw.githubusercontent.com/BasedHardware/Friend/main/scripts/install-deps.sh | bash + +# Clone and setup +git clone https://github.com/BasedHardware/Friend.git chronicle +cd chronicle +make wizard +``` + +The dependency installer will: +- Install Git, Make, curl via Homebrew +- Install Docker Desktop (or prompt you to install manually) +- Verify everything is working + +--- + +## ๐Ÿง Linux + +**Installation for Linux users (Ubuntu/Debian)** + +### Quick Install + +```bash +# Install dependencies automatically +curl -fsSL https://raw.githubusercontent.com/BasedHardware/Friend/main/scripts/install-deps.sh | bash + +# Clone and setup +git clone https://github.com/BasedHardware/Friend.git chronicle +cd chronicle +make wizard +``` + +The dependency installer will: +- Install Git, Make, curl, wget +- Install Docker Engine and Docker Compose +- Add you to the docker group +- Start Docker service + +**Manual Installation (if you prefer)** + +```bash +# Update package lists +sudo apt-get update + +# Install basic tools +sudo apt-get install -y git make curl wget ca-certificates gnupg + +# Install Docker +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh +sudo usermod -aG docker $USER +newgrp docker + +# Clone and setup Chronicle +git clone https://github.com/BasedHardware/Friend.git friend-lite +cd friend-lite +make wizard +``` + +--- + +## What the Dependency Installer Does + +The `install-deps.sh` script automatically: + +โœ… **Detects your operating system** (Ubuntu, Debian, macOS, or WSL2) + +โœ… **Installs required tools:** +- Git (version control) +- Make (build automation) +- curl (HTTP client) +- Docker & Docker Compose (container platform) + +โœ… **Verifies everything works** and shows you the versions + +โœ… **Provides guidance** for Docker Desktop on WSL2/macOS + +โœ… **Smart about Docker:** +- On **WSL2**: Recommends Docker Desktop, or offers to install Docker Engine +- On **Linux**: Installs Docker Engine directly +- On **macOS**: Offers to install Docker Desktop via Homebrew + +--- + +## After Installation + +Once dependencies are installed, run the setup wizard: + +```bash +cd chronicle +make wizard +``` + +The wizard will guide you through: +1. ๐Ÿ” Configuring API keys and passwords +2. ๐ŸŒ Optional Tailscale setup (for remote access) +3. ๐Ÿ“ฆ Creating your environment configuration +4. ๐Ÿš€ Starting Chronicle + +--- + +## What You'll Need + +Before running `make wizard`, have these ready: + +### Required +- **OpenAI API Key** (for memory extraction): https://platform.openai.com/api-keys +- **Deepgram API Key** (for transcription): https://console.deepgram.com/ +- **Admin password** (choose a secure password for your Chronicle account) + +### Optional +- **Mistral API Key** (alternative transcription): https://console.mistral.ai/ +- **Hugging Face Token** (speaker recognition): https://huggingface.co/settings/tokens +- **Tailscale account** (remote access): https://login.tailscale.com/start + +--- + +## System Requirements + +### Minimum +- **CPU**: 2 cores +- **RAM**: 4GB +- **Disk**: 10GB free space +- **OS**: + - Windows 10 (version 2004+) or Windows 11 + - macOS 10.15 (Catalina) or higher + - Ubuntu 20.04+ or Debian 11+ + +### Recommended +- **CPU**: 4+ cores +- **RAM**: 8GB+ +- **Disk**: 20GB+ free space +- **SSD** for better Docker performance + +### For Speaker Recognition (Optional) +- **RAM**: 8GB+ recommended +- **GPU**: NVIDIA GPU with CUDA support (optional, improves performance) + +--- + +## Troubleshooting + +### "curl: command not found" + +**Linux/WSL2:** +```bash +sudo apt-get update +sudo apt-get install curl +``` + +**macOS:** +```bash +# curl is pre-installed, but if missing: +brew install curl +``` + +### "Permission denied" when running docker + +**You need to log out and back in** after the installer adds you to the docker group. + +Or run: +```bash +newgrp docker +``` + +### Docker Desktop not starting on Windows + +1. Enable virtualization in BIOS +2. Enable WSL2: `wsl --install` +3. Restart computer +4. Start Docker Desktop + +### Docker not found in WSL2 + +Make sure Docker Desktop has WSL2 integration enabled: +1. Open Docker Desktop +2. Settings โ†’ Resources โ†’ WSL Integration +3. Enable "Ubuntu-22.04" +4. Click "Apply & Restart" + +--- + +## Advanced: Manual Dependency Installation + +If you prefer not to use the automated script, see: +- **Windows**: [WINDOWS-SETUP.md](WINDOWS-SETUP.md) - Step-by-step manual instructions +- **Linux**: [Docker Install Docs](https://docs.docker.com/engine/install/) +- **macOS**: [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) + +--- + +## Getting Help + +- **GitHub Issues**: https://github.com/BasedHardware/Friend/issues +- **Documentation**: Check `CLAUDE.md` for comprehensive docs +- **Setup Wizard Issues**: See `WIZARD.md` for wizard-specific help + +--- + +## Next Steps + +After installation: + +1. **Start Chronicle**: `./start-env.sh dev` +2. **Access Web Dashboard**: http://localhost:5173 +3. **Check API Docs**: http://localhost:8000/docs +4. **Connect Your Device**: Follow the mobile app setup guide + +Welcome to Chronicle! ๐Ÿš€ diff --git a/Makefile b/Makefile index 984549f1..f727cb08 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ endif SCRIPTS_DIR := scripts K8S_SCRIPTS_DIR := $(SCRIPTS_DIR)/k8s -.PHONY: help menu wizard setup-secrets setup-tailscale setup-environment check-secrets setup-k8s setup-infrastructure setup-rbac setup-storage-pvc config config-docker config-k8s config-all clean deploy deploy-docker deploy-k8s deploy-k8s-full deploy-infrastructure deploy-apps check-infrastructure check-apps build-backend up-backend down-backend k8s-status k8s-cleanup k8s-purge audio-manage mycelia-sync-status mycelia-sync-all mycelia-sync-user mycelia-check-orphans mycelia-reassign-orphans test-robot test-robot-integration test-robot-unit test-robot-endpoints test-robot-specific test-robot-clean +.PHONY: help menu wizard setup-secrets setup-tailscale configure-tailscale-serve setup-environment check-secrets setup-k8s setup-infrastructure setup-rbac setup-storage-pvc config config-docker config-k8s config-all clean deploy deploy-docker deploy-k8s deploy-k8s-full deploy-infrastructure deploy-apps check-infrastructure check-apps build-backend up-backend down-backend k8s-status k8s-cleanup k8s-purge audio-manage mycelia-sync-status mycelia-sync-all mycelia-sync-user mycelia-check-orphans mycelia-reassign-orphans mycelia-create-token test-robot test-robot-integration test-robot-unit test-robot-endpoints test-robot-specific test-robot-clean infra-start infra-stop infra-restart infra-logs infra-status infra-clean caddy-start caddy-stop caddy-restart caddy-logs caddy-status caddy-regenerate env-list env-start env-stop env-clean env-status # Default target .DEFAULT_GOAL := menu @@ -43,10 +43,11 @@ menu: ## Show interactive menu (default) @echo "================================" @echo @echo "๐Ÿง™ Setup:" - @echo " wizard ๐Ÿง™ Interactive setup wizard (secrets + Tailscale + environment)" - @echo " setup-secrets ๐Ÿ” Configure API keys and passwords" - @echo " setup-tailscale ๐ŸŒ Configure Tailscale for distributed deployment" - @echo " setup-environment ๐Ÿ“ฆ Create a custom environment" + @echo " wizard ๐Ÿง™ Interactive setup wizard (secrets + Tailscale + environment)" + @echo " setup-secrets ๐Ÿ” Configure API keys and passwords" + @echo " setup-tailscale ๐ŸŒ Configure Tailscale for distributed deployment" + @echo " configure-tailscale-serve ๐ŸŒ Configure Tailscale serve routes (single environment)" + @echo " setup-environment ๐Ÿ“ฆ Create a custom environment" @echo @echo "๐Ÿ“‹ Quick Actions:" @echo " setup-dev ๐Ÿ› ๏ธ Setup development environment (git hooks, pre-commit)" @@ -78,12 +79,29 @@ menu: ## Show interactive menu (default) @echo " clean ๐Ÿงน Clean up generated files" @echo @echo "๐Ÿ”„ Mycelia Sync:" + @echo " mycelia-create-token ๐Ÿ”‘ Create Mycelia API token for a user" @echo " mycelia-sync-status ๐Ÿ“Š Show Mycelia OAuth sync status" @echo " mycelia-sync-all ๐Ÿ”„ Sync all Friend-Lite users to Mycelia" @echo " mycelia-sync-user ๐Ÿ‘ค Sync specific user (EMAIL=user@example.com)" @echo " mycelia-check-orphans ๐Ÿ” Find orphaned Mycelia objects" @echo " mycelia-reassign-orphans โ™ป๏ธ Reassign orphans (EMAIL=admin@example.com)" @echo + @echo "๐Ÿ—๏ธ Shared Infrastructure:" + @echo " infra-start ๐Ÿš€ Start shared infrastructure (MongoDB, Redis, Qdrant, optional Neo4j)" + @echo " infra-stop ๐Ÿ›‘ Stop infrastructure" + @echo " infra-restart ๐Ÿ”„ Restart infrastructure" + @echo " infra-status ๐Ÿ“Š Check infrastructure status" + @echo " infra-logs ๐Ÿ“‹ View infrastructure logs" + @echo " infra-clean ๐Ÿ—‘๏ธ Clean all infrastructure data (DANGER!)" + @echo + @echo "๐ŸŒ Caddy Reverse Proxy (Shared Service):" + @echo " caddy-start ๐Ÿš€ Start shared Caddy (serves all environments)" + @echo " caddy-stop ๐Ÿ›‘ Stop Caddy" + @echo " caddy-restart ๐Ÿ”„ Restart Caddy" + @echo " caddy-status ๐Ÿ“Š Check if Caddy is running" + @echo " caddy-logs ๐Ÿ“‹ View Caddy logs" + @echo " caddy-regenerate ๐Ÿ”ง Regenerate Caddyfile from environments" + @echo @echo "Current configuration:" @echo " DOMAIN: $(DOMAIN)" @echo " DEPLOYMENT_MODE: $(DEPLOYMENT_MODE)" @@ -129,6 +147,7 @@ help: ## Show detailed help for all targets @echo " audio-manage Interactive audio file management" @echo @echo "๐Ÿ”„ MYCELIA SYNC:" + @echo " mycelia-create-token Create Mycelia API token for a user" @echo " mycelia-sync-status Show Mycelia OAuth sync status for all users" @echo " mycelia-sync-all Sync all Friend-Lite users to Mycelia OAuth" @echo " mycelia-sync-user Sync specific user (EMAIL=user@example.com)" @@ -184,18 +203,20 @@ wizard: ## ๐Ÿง™ Interactive setup wizard - guides through complete Friend-Lite s @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" @echo "" @echo "This wizard will guide you through:" - @echo " 1. ๐Ÿ” Setting up secrets (API keys, passwords)" - @echo " 2. ๐ŸŒ Optionally configuring Tailscale for distributed deployment" - @echo " 3. ๐Ÿ“ฆ Creating a custom environment" - @echo " 4. ๐Ÿš€ Starting your Friend-Lite instance" + @echo " 1. ๐Ÿ“ฆ Creating your environment (name, ports, services)" + @echo " 2. ๐Ÿ” Configuring secrets (API keys based on your services)" + @echo " 3. ๐ŸŒ Optionally configuring Tailscale for remote access" + @echo " 4. ๐Ÿ”ง Finalizing setup (certificates, final configuration)" @echo "" @read -p "Press Enter to continue or Ctrl+C to exit..." @echo "" + @$(MAKE) --no-print-directory setup-environment + @echo "" @$(MAKE) --no-print-directory setup-secrets @echo "" @$(MAKE) --no-print-directory setup-tailscale @echo "" - @$(MAKE) --no-print-directory setup-environment + @$(MAKE) --no-print-directory finalize-setup @echo "" @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" @echo "โœ… Setup Complete!" @@ -204,12 +225,11 @@ wizard: ## ๐Ÿง™ Interactive setup wizard - guides through complete Friend-Lite s @echo "๐Ÿš€ Next Steps:" @echo "" @if [ -f ".env.secrets" ] && [ -d "environments" ]; then \ + LATEST_ENV=$$(ls -t environments/*.env 2>/dev/null | head -1 | xargs basename -s .env 2>/dev/null || echo "dev"); \ echo " Start your environment:"; \ - echo " ./start-env.sh $${ENV_NAME:-dev}"; \ + echo " ./start-env.sh $$LATEST_ENV"; \ echo ""; \ - echo " Or with optional services:"; \ - echo " ./start-env.sh $${ENV_NAME:-dev} --profile mycelia"; \ - echo " ./start-env.sh $${ENV_NAME:-dev} --profile speaker"; \ + echo " ๐Ÿ’ก Your configured services will start automatically!"; \ else \ echo " โš ๏ธ Some setup steps were skipped. Run individual targets:"; \ echo " make setup-secrets"; \ @@ -234,378 +254,19 @@ check-secrets: ## Check if secrets file exists and is configured @echo "โœ… Secrets file configured" setup-secrets: ## ๐Ÿ” Interactive secrets setup (API keys, passwords) - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "๐Ÿ” Step 1: Secrets Configuration" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "" - @if [ -f ".env.secrets" ]; then \ - echo "โ„น๏ธ .env.secrets already exists"; \ - echo ""; \ - read -p "Do you want to reconfigure it? (y/N): " reconfigure; \ - if [ "$$reconfigure" != "y" ] && [ "$$reconfigure" != "Y" ]; then \ - echo ""; \ - echo "โœ… Keeping existing secrets"; \ - exit 0; \ - fi; \ - echo ""; \ - echo "๐Ÿ“ Backing up existing .env.secrets..."; \ - cp .env.secrets .env.secrets.backup.$$(date +%Y%m%d_%H%M%S); \ - echo ""; \ - else \ - echo "๐Ÿ“ Creating .env.secrets from template..."; \ - cp .env.secrets.template .env.secrets; \ - echo "โœ… Created .env.secrets"; \ - echo ""; \ - fi - @echo "๐Ÿ”‘ Required Secrets Configuration" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "" - @echo "Let's configure your secrets. Press Enter to skip optional ones." - @echo "" - @# JWT Secret Key (required) - @echo "1๏ธโƒฃ JWT Secret Key (required for authentication)" - @echo " This is used to sign JWT tokens. Should be random and secure." - @read -p " Enter JWT secret key (or press Enter to generate): " jwt_key; \ - if [ -z "$$jwt_key" ]; then \ - jwt_key=$$(openssl rand -hex 32 2>/dev/null || cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 64 | head -n 1); \ - echo " โœ… Generated random key: $$jwt_key"; \ - fi; \ - sed -i.bak "s|^AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=$$jwt_key|" .env.secrets && rm .env.secrets.bak - @echo "" - @# Admin credentials - @echo "2๏ธโƒฃ Admin Account" - @read -p " Admin email (default: admin@example.com): " admin_email; \ - admin_email=$${admin_email:-admin@example.com}; \ - sed -i.bak "s|^ADMIN_EMAIL=.*|ADMIN_EMAIL=$$admin_email|" .env.secrets && rm .env.secrets.bak; \ - read -sp " Admin password: " admin_pass; echo ""; \ - if [ -n "$$admin_pass" ]; then \ - sed -i.bak "s|^ADMIN_PASSWORD=.*|ADMIN_PASSWORD=$$admin_pass|" .env.secrets && rm .env.secrets.bak; \ - fi - @echo "" - @# OpenAI API Key - @echo "3๏ธโƒฃ OpenAI API Key (required for memory extraction)" - @echo " Get your key from: https://platform.openai.com/api-keys" - @read -p " OpenAI API key (or press Enter to skip): " openai_key; \ - if [ -n "$$openai_key" ]; then \ - sed -i.bak "s|^OPENAI_API_KEY=.*|OPENAI_API_KEY=$$openai_key|" .env.secrets && rm .env.secrets.bak; \ - fi - @echo "" - @# Deepgram API Key - @echo "4๏ธโƒฃ Deepgram API Key (recommended for transcription)" - @echo " Get your key from: https://console.deepgram.com/" - @read -p " Deepgram API key (or press Enter to skip): " deepgram_key; \ - if [ -n "$$deepgram_key" ]; then \ - sed -i.bak "s|^DEEPGRAM_API_KEY=.*|DEEPGRAM_API_KEY=$$deepgram_key|" .env.secrets && rm .env.secrets.bak; \ - fi - @echo "" - @# Optional: Mistral API Key - @echo "5๏ธโƒฃ Mistral API Key (optional - alternative transcription)" - @echo " Get your key from: https://console.mistral.ai/" - @read -p " Mistral API key (or press Enter to skip): " mistral_key; \ - if [ -n "$$mistral_key" ]; then \ - sed -i.bak "s|^MISTRAL_API_KEY=.*|MISTRAL_API_KEY=$$mistral_key|" .env.secrets && rm .env.secrets.bak; \ - fi - @echo "" - @# Optional: Hugging Face Token - @echo "6๏ธโƒฃ Hugging Face Token (optional - for speaker recognition models)" - @echo " Get your token from: https://huggingface.co/settings/tokens" - @read -p " HF token (or press Enter to skip): " hf_token; \ - if [ -n "$$hf_token" ]; then \ - sed -i.bak "s|^HF_TOKEN=.*|HF_TOKEN=$$hf_token|" .env.secrets && rm .env.secrets.bak; \ - fi - @echo "" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "โœ… Secrets configured successfully!" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "" - @echo "๐Ÿ“„ Configuration saved to: .env.secrets" - @echo "๐Ÿ”’ This file is gitignored and will not be committed" - @echo "" + @./scripts/setup-secrets.sh setup-tailscale: ## ๐ŸŒ Interactive Tailscale setup for distributed deployment - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "๐ŸŒ Step 2: Tailscale Configuration (Optional)" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "" - @echo "Tailscale enables secure distributed deployments:" - @echo " โ€ข Run services on different machines" - @echo " โ€ข Secure service-to-service communication" - @echo " โ€ข Access from mobile devices" - @echo " โ€ข Automatic HTTPS with 'tailscale serve'" - @echo "" - @read -p "Do you want to configure Tailscale? (y/N): " use_tailscale; \ - if [ "$$use_tailscale" != "y" ] && [ "$$use_tailscale" != "Y" ]; then \ - echo ""; \ - echo "โ„น๏ธ Skipping Tailscale setup"; \ - echo " You can run this later with: make setup-tailscale"; \ - exit 0; \ - fi - @echo "" - @# Check if Tailscale is installed - @if ! command -v tailscale >/dev/null 2>&1; then \ - echo "โŒ Tailscale not found"; \ - echo ""; \ - echo "๐Ÿ“ฆ Install Tailscale:"; \ - echo " curl -fsSL https://tailscale.com/install.sh | sh"; \ - echo " sudo tailscale up"; \ - echo ""; \ - echo "Then run this setup again: make setup-tailscale"; \ - exit 1; \ - fi - @echo "โœ… Tailscale is installed" - @echo "" - @# Get Tailscale status - @echo "๐Ÿ“Š Checking Tailscale status..." - @if ! tailscale status >/dev/null 2>&1; then \ - echo "โŒ Tailscale is not running"; \ - echo ""; \ - echo "๐Ÿ”ง Start Tailscale:"; \ - echo " sudo tailscale up"; \ - echo ""; \ - exit 1; \ - fi - @echo "โœ… Tailscale is running" - @echo "" - @echo "๐Ÿ“‹ Your Tailscale devices:" - @echo "" - @tailscale status | head -n 10 - @echo "" - @# Get Tailscale hostname - @echo "๐Ÿท๏ธ Tailscale Hostname Configuration" - @echo "" - @echo "Your Tailscale hostname is the DNS name assigned to THIS machine." - @echo "It's different from the IP address - it's a permanent name." - @echo "" - @echo "๐Ÿ“‹ To find your Tailscale hostname:" - @echo " 1. Run: tailscale status" - @echo " 2. Look for this machine's name in the first column" - @echo " 3. The full hostname is shown on the right (ends in .ts.net)" - @echo "" - @echo "Example output:" - @echo " anubis 100.x.x.x anubis.tail12345.ts.net <-- Your hostname" - @echo "" - @default_hostname=$$(tailscale status --json 2>/dev/null | grep -o '"DNSName":"[^"]*"' | head -1 | cut -d'"' -f4 | sed 's/\.$$//'); \ - if [ -n "$$default_hostname" ]; then \ - echo "๐Ÿ’ก Auto-detected hostname for THIS machine: $$default_hostname"; \ - echo ""; \ - fi; \ - read -p "Tailscale hostname [$$default_hostname]: " tailscale_hostname; \ - tailscale_hostname=$${tailscale_hostname:-$$default_hostname}; \ - if [ -z "$$tailscale_hostname" ]; then \ - echo ""; \ - echo "โŒ No hostname provided"; \ - exit 1; \ - fi; \ - export TAILSCALE_HOSTNAME=$$tailscale_hostname; \ - echo ""; \ - echo "โœ… Using Tailscale hostname: $$tailscale_hostname" - @echo "" - @# SSL Setup - @echo "๐Ÿ” SSL Certificate Configuration" - @echo "" - @echo "How do you want to handle HTTPS?" - @echo " 1) Use 'tailscale serve' (automatic HTTPS, recommended)" - @echo " 2) Generate self-signed certificates" - @echo " 3) Skip SSL setup" - @echo "" - @read -p "Choose option (1-3) [1]: " ssl_choice; \ - ssl_choice=$${ssl_choice:-1}; \ - case $$ssl_choice in \ - 1) \ - echo ""; \ - echo "โœ… Will use 'tailscale serve' for automatic HTTPS"; \ - echo ""; \ - echo "๐Ÿ“ After starting services, run:"; \ - echo " tailscale serve https / http://localhost:8000"; \ - echo " tailscale serve https / http://localhost:5173"; \ - echo ""; \ - export HTTPS_ENABLED=true; \ - ;; \ - 2) \ - echo ""; \ - echo "๐Ÿ” Generating SSL certificates for $$tailscale_hostname..."; \ - if [ -f "backends/advanced/ssl/generate-ssl.sh" ]; then \ - cd backends/advanced && ./ssl/generate-ssl.sh $$tailscale_hostname && cd ../..; \ - echo ""; \ - echo "โœ… SSL certificates generated"; \ - else \ - echo "โŒ SSL generation script not found"; \ - exit 1; \ - fi; \ - export HTTPS_ENABLED=true; \ - ;; \ - 3) \ - echo ""; \ - echo "โ„น๏ธ Skipping SSL setup"; \ - export HTTPS_ENABLED=false; \ - ;; \ - *) \ - echo ""; \ - echo "โŒ Invalid choice"; \ - exit 1; \ - ;; \ - esac - @echo "" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "โœ… Tailscale configuration complete!" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "" + @./scripts/setup-tailscale.sh + +configure-tailscale-serve: ## ๐ŸŒ Configure Tailscale serve for an environment + @./scripts/configure-tailscale-serve.sh setup-environment: ## ๐Ÿ“ฆ Create a custom environment configuration - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "๐Ÿ“ฆ Step 3: Environment Setup" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "" - @echo "Environments allow you to:" - @echo " โ€ข Run multiple isolated instances (dev, staging, prod)" - @echo " โ€ข Use different databases and ports for each" - @echo " โ€ข Test changes without affecting production" - @echo "" - @# Check existing environments - @if [ -d "environments" ] && [ -n "$$(ls -A environments/*.env 2>/dev/null)" ]; then \ - echo "๐Ÿ“‹ Existing environments:"; \ - ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$$||' | sed 's/^/ - /'; \ - echo ""; \ - fi - @# Get environment name - @read -p "Environment name [dev]: " env_name; \ - env_name=$${env_name:-dev}; \ - mkdir -p environments; \ - env_file="environments/$$env_name.env"; \ - echo ""; \ - if [ -f "$$env_file" ]; then \ - echo "โš ๏ธ Environment '$$env_name' already exists"; \ - read -p "Do you want to overwrite it? (y/N): " overwrite; \ - if [ "$$overwrite" != "y" ] && [ "$$overwrite" != "Y" ]; then \ - echo ""; \ - echo "โ„น๏ธ Keeping existing environment"; \ - exit 0; \ - fi; \ - echo ""; \ - cp "$$env_file" "$$env_file.backup.$$(date +%Y%m%d_%H%M%S)"; \ - echo "๐Ÿ“ Backed up existing environment"; \ - echo ""; \ - fi - @# Get port offset - @echo "๐Ÿ”ข Port Configuration"; \ - echo ""; \ - echo "Each environment needs a unique port offset to avoid conflicts."; \ - echo " dev: 0 (8000, 5173, 27017, ...)"; \ - echo " staging: 100 (8100, 5273, 27117, ...)"; \ - echo " prod: 200 (8200, 5373, 27217, ...)"; \ - echo ""; \ - read -p "Port offset [0]: " port_offset; \ - port_offset=$${port_offset:-0}; \ - echo "" - @# Get database names - @echo "๐Ÿ’พ Database Configuration"; \ - echo ""; \ - read -p "MongoDB database name [friend-lite-$$env_name]: " mongodb_db; \ - mongodb_db=$${mongodb_db:-friend-lite-$$env_name}; \ - read -p "Mycelia database name [mycelia-$$env_name]: " mycelia_db; \ - mycelia_db=$${mycelia_db:-mycelia-$$env_name}; \ - echo "" - @# Optional services - @echo "๐Ÿ”Œ Optional Services"; \ - echo ""; \ - read -p "Enable Mycelia? (y/N): " enable_mycelia; \ - read -p "Enable Speaker Recognition? (y/N): " enable_speaker; \ - read -p "Enable OpenMemory MCP? (y/N): " enable_openmemory; \ - read -p "Enable Parakeet ASR? (y/N): " enable_parakeet; \ - services=""; \ - if [ "$$enable_mycelia" = "y" ] || [ "$$enable_mycelia" = "Y" ]; then \ - services="$$services mycelia"; \ - fi; \ - if [ "$$enable_speaker" = "y" ] || [ "$$enable_speaker" = "Y" ]; then \ - services="$$services speaker"; \ - fi; \ - if [ "$$enable_openmemory" = "y" ] || [ "$$enable_openmemory" = "Y" ]; then \ - services="$$services openmemory"; \ - fi; \ - if [ "$$enable_parakeet" = "y" ] || [ "$$enable_parakeet" = "Y" ]; then \ - services="$$services parakeet"; \ - fi; \ - echo "" - @# Tailscale settings (from previous step or ask) - @if [ -n "$$TAILSCALE_HOSTNAME" ]; then \ - echo ""; \ - echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"; \ - echo "๐ŸŒ Tailscale Configuration"; \ - echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"; \ - echo ""; \ - echo "โœ… Using Tailscale configuration from previous step:"; \ - echo " Hostname: $$TAILSCALE_HOSTNAME"; \ - echo " HTTPS: $$HTTPS_ENABLED"; \ - echo ""; \ - tailscale_hostname=$$TAILSCALE_HOSTNAME; \ - https_enabled=$$HTTPS_ENABLED; \ - else \ - echo ""; \ - echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"; \ - echo "๐ŸŒ Tailscale Configuration (Optional)"; \ - echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"; \ - echo ""; \ - echo "โš ๏ธ You skipped Tailscale setup earlier."; \ - echo ""; \ - echo "You can still configure it for this environment:"; \ - echo " โ€ข Enter your Tailscale hostname (from 'tailscale status')"; \ - echo " โ€ข Or press Enter to skip (HTTP only, no Tailscale)"; \ - echo ""; \ - read -p "Tailscale hostname (or press Enter to skip): " tailscale_hostname; \ - if [ -n "$$tailscale_hostname" ]; then \ - echo ""; \ - echo "โš ๏ธ Note: SSL certificates were not generated."; \ - echo " To generate them later, run:"; \ - echo " cd backends/advanced && ./ssl/generate-ssl.sh $$tailscale_hostname"; \ - echo ""; \ - https_enabled=true; \ - else \ - https_enabled=false; \ - fi; \ - fi; \ - echo "" - @# Write environment file - @echo "๐Ÿ“ Creating environment file: $$env_file"; \ - echo ""; \ - printf "# ========================================\n" > "$$env_file"; \ - printf "# Friend-Lite Environment: %s\n" "$$env_name" >> "$$env_file"; \ - printf "# ========================================\n" >> "$$env_file"; \ - printf "# Generated: %s\n" "$$(date)" >> "$$env_file"; \ - printf "\n" >> "$$env_file"; \ - printf "# Environment identification\n" >> "$$env_file"; \ - printf "ENV_NAME=%s\n" "$$env_name" >> "$$env_file"; \ - printf "COMPOSE_PROJECT_NAME=friend-lite-%s\n" "$$env_name" >> "$$env_file"; \ - printf "\n" >> "$$env_file"; \ - printf "# Port offset (each environment needs unique ports)\n" >> "$$env_file"; \ - printf "PORT_OFFSET=%s\n" "$$port_offset" >> "$$env_file"; \ - printf "\n" >> "$$env_file"; \ - printf "# Data directory (isolated per environment)\n" >> "$$env_file"; \ - printf "DATA_DIR=./data/%s\n" "$$env_name" >> "$$env_file"; \ - printf "\n" >> "$$env_file"; \ - printf "# Database names (isolated per environment)\n" >> "$$env_file"; \ - printf "MONGODB_DATABASE=%s\n" "$$mongodb_db" >> "$$env_file"; \ - printf "MYCELIA_DB=%s\n" "$$mycelia_db" >> "$$env_file"; \ - printf "\n" >> "$$env_file"; \ - printf "# Optional services\n" >> "$$env_file"; \ - printf "SERVICES=%s\n" "$$services" >> "$$env_file"; \ - printf "\n" >> "$$env_file"; \ - if [ -n "$$tailscale_hostname" ]; then \ - printf "# Tailscale configuration\n" >> "$$env_file"; \ - printf "TAILSCALE_HOSTNAME=%s\n" "$$tailscale_hostname" >> "$$env_file"; \ - printf "HTTPS_ENABLED=%s\n" "$$https_enabled" >> "$$env_file"; \ - printf "\n" >> "$$env_file"; \ - fi; \ - echo "โœ… Environment created: $$env_name" - @echo "" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "โœ… Environment setup complete!" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "" - @echo "๐Ÿ“„ Environment file: $$env_file" - @echo "" - @echo "๐Ÿš€ Start this environment with:" - @echo " ./start-env.sh $$env_name" - @echo "" + @./scripts/setup-environment.sh + +finalize-setup: ## ๐Ÿ”ง Finalize setup (generate Caddyfile, provision certificates) + @./scripts/finalize-setup.sh # ======================================== # KUBERNETES SETUP @@ -838,6 +499,39 @@ mycelia-reassign-orphans: ## Reassign orphaned objects to user (usage: make myce @read -p "Continue? (y/N): " confirm && [ "$$confirm" = "y" ] || exit 1 @cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --reassign-orphans --target-email $(EMAIL) +mycelia-create-token: ## Create Mycelia API token for a user in specified environment + @echo "๐Ÿ”‘ Creating Mycelia API Token" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "" + @# List available environments + @if [ ! -d "environments" ] || [ -z "$$(ls -A environments/*.env 2>/dev/null)" ]; then \ + echo "โŒ No environments found. Create one with: make wizard"; \ + exit 1; \ + fi + @echo "๐Ÿ“‹ Available environments:"; \ + ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$$||' | sed 's/^/ - /'; \ + echo "" + @# Ask for environment + @read -p "Environment name: " env_name; \ + if [ ! -f "environments/$$env_name.env" ]; then \ + echo "โŒ Environment '$$env_name' not found"; \ + exit 1; \ + fi; \ + echo ""; \ + echo "๐Ÿ“ฆ Checking if $$env_name environment is running..."; \ + echo ""; \ + source "environments/$$env_name.env"; \ + running=$$(docker ps --filter "name=$$COMPOSE_PROJECT_NAME-friend-backend-1" --format "{{.Names}}" 2>/dev/null); \ + if [ -z "$$running" ]; then \ + echo "โš ๏ธ Environment not running. Start it first with:"; \ + echo " ./start-env.sh $$env_name"; \ + echo ""; \ + exit 1; \ + fi; \ + echo "โœ… Environment is running ($$COMPOSE_PROJECT_NAME)"; \ + echo ""; \ + cd backends/advanced && ENV_NAME=$$env_name uv run python scripts/create_mycelia_api_key.py + # ======================================== # TESTING TARGETS # ======================================== @@ -941,3 +635,171 @@ env-status: ## Show status of all environments echo ""; \ done +# ======================================== +# SHARED INFRASTRUCTURE (MongoDB, Redis, Qdrant) +# ======================================== + +infra-start: ## Start shared infrastructure (MongoDB, Redis, Qdrant, optional Neo4j) + @echo "๐Ÿš€ Starting shared infrastructure services..." + @echo "" + @# Check if network exists, create if not + @docker network inspect chronicle-network >/dev/null 2>&1 || docker network create chronicle-network + @# Check if Neo4j should be started (NEO4J_ENABLED in any environment) + @if grep -q "^NEO4J_ENABLED=true" environments/*.env 2>/dev/null; then \ + echo "๐Ÿ”— Neo4j enabled in at least one environment - starting with Neo4j profile..."; \ + docker compose -p chronicle-infra -f compose/infrastructure-shared.yml --profile neo4j up -d; \ + else \ + docker compose -p chronicle-infra -f compose/infrastructure-shared.yml up -d; \ + fi + @echo "" + @echo "โœ… Infrastructure services started!" + @echo "" + @echo " ๐Ÿ“Š MongoDB: mongodb://localhost:27017" + @echo " ๐Ÿ’พ Redis: redis://localhost:6379" + @echo " ๐Ÿ” Qdrant: http://localhost:6034" + @if docker ps --format '{{.Names}}' | grep -q '^chronicle-neo4j$$'; then \ + echo " ๐Ÿ”— Neo4j: http://localhost:7474 (bolt: 7687)"; \ + fi + @echo "" + @echo "๐Ÿ’ก These services are shared by all environments" + @echo " Each environment uses unique database names for isolation" + @echo "" + +infra-stop: ## Stop shared infrastructure + @echo "๐Ÿ›‘ Stopping shared infrastructure..." + @echo "โš ๏ธ This will affect ALL running environments!" + @read -p "Continue? (y/N): " confirm && [ "$$confirm" = "y" ] || exit 1 + @docker compose -p chronicle-infra -f compose/infrastructure-shared.yml down + @echo "โœ… Infrastructure stopped" + +infra-restart: ## Restart shared infrastructure + @echo "๐Ÿ”„ Restarting shared infrastructure..." + @docker compose -p chronicle-infra -f compose/infrastructure-shared.yml restart + @echo "โœ… Infrastructure restarted" + +infra-logs: ## View infrastructure logs + @echo "๐Ÿ“‹ Viewing infrastructure logs (press Ctrl+C to exit)..." + @docker compose -p chronicle-infra -f compose/infrastructure-shared.yml logs -f + +infra-status: ## Check infrastructure status + @echo "๐Ÿ“Š Infrastructure Status:" + @echo "" + @if docker ps --format '{{.Names}}' | grep -qE '(chronicle|friend-lite).*mongo'; then \ + echo "โœ… MongoDB is running"; \ + docker ps --format '{{.Names}} {{.Ports}}' | grep mongo | awk '{print " " $$1}'; \ + else \ + echo "โŒ MongoDB is not running"; \ + fi + @echo "" + @if docker ps --format '{{.Names}}' | grep -qE '(chronicle|friend-lite).*redis'; then \ + echo "โœ… Redis is running"; \ + docker ps --format '{{.Names}} {{.Ports}}' | grep redis | awk '{print " " $$1}'; \ + else \ + echo "โŒ Redis is not running"; \ + fi + @echo "" + @if docker ps --format '{{.Names}}' | grep -qE '(chronicle|friend-lite).*qdrant'; then \ + echo "โœ… Qdrant is running"; \ + docker ps --format '{{.Names}} {{.Ports}}' | grep qdrant | awk '{print " " $$1}'; \ + else \ + echo "โŒ Qdrant is not running"; \ + fi + @echo "" + @if docker ps --format '{{.Names}}' | grep -q '^chronicle-neo4j$$'; then \ + echo "โœ… Neo4j is running"; \ + docker ps --format '{{.Names}} {{.Ports}}' | grep neo4j | awk '{print " " $$1}'; \ + else \ + echo "โ„น๏ธ Neo4j is not running (optional)"; \ + fi + @echo "" + @if ! docker ps --format '{{.Names}}' | grep -qE '(chronicle|friend-lite).*(mongo|redis|qdrant)'; then \ + echo "๐Ÿ’ก Start infrastructure with: make infra-start"; \ + fi + +infra-clean: ## Clean infrastructure data (DANGER: deletes all databases!) + @echo "โš ๏ธ WARNING: This will delete ALL data from ALL environments!" + @echo " This includes:" + @echo " โ€ข All MongoDB databases" + @echo " โ€ข All Redis data" + @echo " โ€ข All Qdrant collections" + @echo " โ€ข All Neo4j graph databases (if enabled)" + @echo "" + @read -p "Type 'DELETE ALL DATA' to confirm: " confirm && [ "$$confirm" = "DELETE ALL DATA" ] || exit 1 + @docker compose -p chronicle-infra -f compose/infrastructure-shared.yml --profile neo4j down -v + @echo "โœ… Infrastructure data deleted" + +# ======================================== +# CADDY REVERSE PROXY (Shared Service) +# ======================================== + +caddy-start: ## Start shared Caddy reverse proxy (serves all environments) + @echo "๐Ÿš€ Starting Caddy reverse proxy..." + @echo "" + @# Check if Caddyfile exists + @if [ ! -f "caddy/Caddyfile" ]; then \ + echo "โš ๏ธ Caddyfile not found. Generating..."; \ + ./scripts/generate-caddyfile.sh; \ + echo ""; \ + fi + @# Start Caddy + @docker compose -f compose/caddy.yml up -d + @echo "" + @echo "โœ… Caddy reverse proxy started!" + @echo "" + @# Show access URLs + @if [ -f "config-docker.env" ]; then \ + source config-docker.env; \ + if [ -n "$$TAILSCALE_HOSTNAME" ]; then \ + echo "๐ŸŒ Access your environments at:"; \ + echo " https://$$TAILSCALE_HOSTNAME/"; \ + echo ""; \ + echo " Individual environments:"; \ + for env in $$(ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$$||'); do \ + echo " โ€ข $$env: https://$$TAILSCALE_HOSTNAME/$$env/"; \ + done; \ + echo ""; \ + fi; \ + fi + +caddy-stop: ## Stop shared Caddy reverse proxy + @echo "๐Ÿ›‘ Stopping Caddy reverse proxy..." + @docker compose -f compose/caddy.yml down + @echo "โœ… Caddy stopped" + +caddy-restart: ## Restart shared Caddy reverse proxy + @echo "๐Ÿ”„ Restarting Caddy reverse proxy..." + @docker compose -f compose/caddy.yml restart + @echo "โœ… Caddy restarted" + +caddy-logs: ## View Caddy logs + @echo "๐Ÿ“‹ Viewing Caddy logs (press Ctrl+C to exit)..." + @docker compose -f compose/caddy.yml logs -f + +caddy-status: ## Check if Caddy is running + @echo "๐Ÿ“Š Caddy Status:" + @echo "" + @if docker ps --format '{{.Names}}' | grep -qE '^(chronicle|friend-lite)-caddy'; then \ + echo "โœ… Caddy is running"; \ + docker ps --format '{{.Names}} {{.Ports}}' | grep caddy | awk '{print " " $$1}'; \ + echo ""; \ + if [ -f "config-docker.env" ]; then \ + source config-docker.env; \ + if [ -n "$$TAILSCALE_HOSTNAME" ]; then \ + echo "๐ŸŒ Access URL: https://$$TAILSCALE_HOSTNAME/"; \ + fi; \ + fi; \ + else \ + echo "โŒ Caddy is not running"; \ + echo " Start with: make caddy-start"; \ + fi + @echo "" + +caddy-regenerate: ## Regenerate Caddyfile from current environments + @echo "๐Ÿ”ง Regenerating Caddyfile..." + @./scripts/generate-caddyfile.sh + @echo "" + @echo "โœ… Caddyfile regenerated" + @echo "" + @echo "๐Ÿ”„ Restart Caddy to apply changes:" + @echo " make caddy-restart" + diff --git a/README.md b/README.md index 0a43076b..fd013d17 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Clone, run setup wizard, start services, access at http://localhost:5173 ## Links +- **๐Ÿ†• [NEW Install Guide](INSTALL.md)** - Complete beginner-friendly setup for Windows, macOS, and Linux - **๐Ÿ“š [Setup Guide](quickstart.md)** - Start here - **๐Ÿ”ง [Full Documentation](CLAUDE.md)** - Comprehensive reference - **๐Ÿ—๏ธ [Architecture Details](Docs/features.md)** - Technical deep dive diff --git a/backends/advanced/compose/backend.yml b/backends/advanced/compose/backend.yml index 195b3f40..6f9ff773 100644 --- a/backends/advanced/compose/backend.yml +++ b/backends/advanced/compose/backend.yml @@ -17,17 +17,11 @@ services: - ../data:/app/data environment: # Service URLs (Docker internal network) - - REDIS_URL=redis://redis:6379/0 + # Use REDIS_DATABASE (derived from PORT_OFFSET) for environment isolation + - REDIS_URL=redis://chronicle-redis:6379/${REDIS_DATABASE:-0} - MYCELIA_URL=${MYCELIA_URL:-http://mycelia-backend:5173} # Complex defaults - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3010,http://localhost:3015,http://localhost:3020,http://localhost:8000} - depends_on: - qdrant: - condition: service_started - mongo: - condition: service_healthy - redis: - condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/readiness"] interval: 30s @@ -56,14 +50,8 @@ services: - ../data:/app/data environment: # Service URLs (Docker internal network) - - REDIS_URL=redis://redis:6379/0 - depends_on: - redis: - condition: service_healthy - mongo: - condition: service_healthy - qdrant: - condition: service_started + # Use REDIS_DATABASE (derived from PORT_OFFSET) for environment isolation + - REDIS_URL=redis://chronicle-redis:6379/${REDIS_DATABASE:-0} restart: unless-stopped networks: - chronicle-network diff --git a/backends/advanced/compose/frontend.yml b/backends/advanced/compose/frontend.yml index 9c18868c..440469fd 100644 --- a/backends/advanced/compose/frontend.yml +++ b/backends/advanced/compose/frontend.yml @@ -7,8 +7,7 @@ services: context: ../webui dockerfile: Dockerfile args: - - VITE_BACKEND_URL=${VITE_BACKEND_URL:-http://localhost:8000} - - BACKEND_URL=${BACKEND_URL:-http://localhost:8000} + - VITE_BASE_PATH=${VITE_BASE_PATH:-/} ports: - "${WEBUI_PORT:-3010}:80" depends_on: diff --git a/backends/advanced/compose/optional-services.yml b/backends/advanced/compose/optional-services.yml index cb188820..b6d19131 100644 --- a/backends/advanced/compose/optional-services.yml +++ b/backends/advanced/compose/optional-services.yml @@ -1,24 +1,10 @@ # Optional Services # Services that can be enabled via profiles or are commented out by default +# All services below are commented out by default - uncomment as needed -services: - # Caddy reverse proxy - provides HTTPS for microphone access - # Access at: https://localhost (accepts self-signed cert warning) - # Enable with: docker compose --profile https up - caddy: - image: caddy:2-alpine - ports: - - "443:443" - - "80:80" # HTTP redirect to HTTPS - volumes: - - ../Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - restart: unless-stopped - profiles: - - https - networks: - - chronicle-network +services: {} + +# Commented out services below - uncomment as needed: # Ollama - Local LLM service # Uncomment to use local LLM instead of OpenAI @@ -104,10 +90,6 @@ networks: external: true volumes: - caddy_data: - driver: local - caddy_config: - driver: local ollama_data: driver: local neo4j_data: diff --git a/compose/asr-services.yml b/compose/asr-services.yml index 807be71b..6d4c3ef3 100644 --- a/compose/asr-services.yml +++ b/compose/asr-services.yml @@ -6,7 +6,9 @@ services: parakeet-asr: build: context: ../extras/asr-services - dockerfile: Dockerfile.parakeet + dockerfile: Dockerfile_Parakeet + args: + - CUDA_VERSION=${PYTORCH_CUDA_VERSION:-cpu} ports: - "${PARAKEET_PORT:-8767}:8767" environment: diff --git a/compose/caddy.yml b/compose/caddy.yml new file mode 100644 index 00000000..a52bfd94 --- /dev/null +++ b/compose/caddy.yml @@ -0,0 +1,43 @@ +# Shared Caddy Reverse Proxy +# Serves ALL Friend-Lite environments via path-based routing +# +# Usage: +# Start Caddy: docker compose -f compose/caddy.yml up -d +# Stop Caddy: docker compose -f compose/caddy.yml down +# View logs: docker compose -f compose/caddy.yml logs -f +# +# This Caddy instance serves multiple environments from a single domain: +# https://hostname/dev/ -> dev environment +# https://hostname/test/ -> test environment +# https://hostname/prod/ -> prod environment +# +# The Caddyfile is auto-generated by scripts/generate-caddyfile.sh + +name: chronicle-proxy + +services: + caddy: + image: caddy:2-alpine + container_name: chronicle-caddy + ports: + - "443:443" + - "80:80" + volumes: + - ../caddy/Caddyfile:/etc/caddy/Caddyfile + - ../certs:/certs:ro + - caddy_data:/data + - caddy_config:/config + restart: unless-stopped + networks: + - chronicle-network + +volumes: + caddy_data: + driver: local + caddy_config: + driver: local + +networks: + chronicle-network: + name: chronicle-network + external: true diff --git a/compose/infrastructure-shared.yml b/compose/infrastructure-shared.yml new file mode 100644 index 00000000..3f0396e9 --- /dev/null +++ b/compose/infrastructure-shared.yml @@ -0,0 +1,128 @@ +# Shared Infrastructure Services +# These services are shared by ALL Friend-Lite environments +# Start once with: docker compose -f compose/infrastructure-shared.yml up -d +# +# Services: +# - MongoDB: Shared database server (port 27017) +# - Redis: Shared cache server (port 6379) +# - Qdrant: Shared vector database (ports 6033/6034) +# - Neo4j: Shared graph database (ports 7474/7687) - optional +# - Caddy: Shared reverse proxy (ports 80/443) +# +# Data Isolation Strategy: +# - MongoDB: Each environment uses unique database name (MONGODB_DATABASE) +# - Redis: Each environment uses unique database number (REDIS_DATABASE) +# - Qdrant: Each environment uses unique collection prefix +# - Neo4j: Each environment uses unique database name (NEO4J_DB) +# - Caddy: Path-based routing to separate environments + +name: chronicle-infra + +services: + mongo: + image: mongo:8.0.14 + container_name: chronicle-mongo + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand({ ping: 1 })"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - chronicle-network + restart: unless-stopped + + redis: + image: redis:7-alpine + container_name: chronicle-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + networks: + - chronicle-network + restart: unless-stopped + + qdrant: + image: qdrant/qdrant:latest + container_name: chronicle-qdrant + ports: + - "6033:6333" # gRPC + - "6034:6334" # HTTP + volumes: + - qdrant_data:/qdrant/storage + networks: + - chronicle-network + restart: unless-stopped + + neo4j: + image: neo4j:2025.10-community + container_name: chronicle-neo4j + ports: + - "7474:7474" # HTTP UI + - "7687:7687" # Bolt protocol + environment: + - NEO4J_AUTH=neo4j/${NEO4J_PASSWORD:-password} + - NEO4J_PLUGINS=["apoc","graph-data-science"] + - NEO4J_dbms_security_procedures_unrestricted=apoc.*,gds.* + - NEO4J_dbms_security_procedures_allowlist=apoc.*,gds.* + volumes: + - neo4j_data:/data + - neo4j_logs:/logs + networks: + - chronicle-network + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider localhost:7474 || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + profiles: + - neo4j + + caddy: + image: caddy:2-alpine + container_name: chronicle-caddy + ports: + - "443:443" + - "80:80" + volumes: + - ../caddy/Caddyfile:/etc/caddy/Caddyfile + - ../certs:/certs:ro + - caddy_data:/data + - caddy_config:/config + restart: unless-stopped + networks: + - chronicle-network + +networks: + chronicle-network: + name: chronicle-network + external: true + +volumes: + mongo_data: + driver: local + redis_data: + driver: local + qdrant_data: + driver: local + neo4j_data: + driver: local + neo4j_logs: + driver: local + caddy_data: + driver: local + caddy_config: + driver: local diff --git a/compose/mycelia.yml b/compose/mycelia.yml index d8ed9384..e0c82949 100644 --- a/compose/mycelia.yml +++ b/compose/mycelia.yml @@ -23,11 +23,6 @@ services: - REDIS_PORT=6379 volumes: - ../extras/mycelia/backend/app:/app/app - depends_on: - mongo: - condition: service_healthy - redis: - condition: service_healthy healthcheck: test: ["CMD", "deno", "eval", "fetch('http://localhost:5100/health').then(r => r.ok ? Deno.exit(0) : Deno.exit(1))"] interval: 30s @@ -38,7 +33,6 @@ services: profiles: - mycelia networks: - - mem0-network - chronicle-network mycelia-frontend: @@ -58,12 +52,9 @@ services: profiles: - mycelia networks: - - mem0-network - chronicle-network networks: - mem0-network: - driver: bridge chronicle-network: name: chronicle-network external: true diff --git a/compose/openmemory.yml b/compose/openmemory.yml index c84ff9c6..c1e222ab 100644 --- a/compose/openmemory.yml +++ b/compose/openmemory.yml @@ -3,18 +3,24 @@ # Enable with: docker compose --profile openmemory up services: - openmemory-mcp: + openmemory: build: - context: ../extras/openmemory-mcp + context: ../extras/openmemory/api dockerfile: Dockerfile ports: - "${OPENMEMORY_PORT:-8765}:8765" environment: - OPENAI_API_KEY=${OPENAI_API_KEY} - - OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory} - - QDRANT_URL=${QDRANT_URL:-http://qdrant:6334} - depends_on: - - qdrant + - USER=${OPENMEMORY_USER_ID:-user} + - API_KEY=${OPENAI_API_KEY} + # Use shared Qdrant instance (same as Friend-Lite) + - QDRANT_HOST=qdrant + - QDRANT_PORT=6333 + # Neo4j configuration (when graph memory is enabled) + - NEO4J_URL=neo4j://neo4j:7687 + - NEO4J_USERNAME=neo4j + - NEO4J_PASSWORD=${NEO4J_PASSWORD:-password} + - NEO4J_DB=neo4j # Neo4j Community Edition only supports single database profiles: - openmemory restart: unless-stopped @@ -29,14 +35,20 @@ services: # OpenMemory UI (optional within openmemory profile) openmemory-ui: + build: + context: ../extras/openmemory/ui + dockerfile: Dockerfile + args: + - NEXT_PUBLIC_API_URL=http://localhost:${OPENMEMORY_PORT:-8765} + - NEXT_PUBLIC_USER_ID=${OPENMEMORY_USER_ID:-user} image: mem0/openmemory-ui:latest ports: - - "${OPENMEMORY_UI_PORT:-3001}:3000" + - "${OPENMEMORY_UI_PORT:-3330}:3000" environment: - NEXT_PUBLIC_API_URL=http://localhost:${OPENMEMORY_PORT:-8765} - - NEXT_PUBLIC_USER_ID=${OPENMEMORY_USER_ID:-openmemory} + - NEXT_PUBLIC_USER_ID=${OPENMEMORY_USER_ID:-user} depends_on: - - openmemory-mcp + - openmemory profiles: - openmemory - openmemory-ui diff --git a/compose/speaker-recognition.yml b/compose/speaker-recognition.yml index c101d14e..9194974f 100644 --- a/compose/speaker-recognition.yml +++ b/compose/speaker-recognition.yml @@ -7,10 +7,16 @@ services: build: context: ../extras/speaker-recognition dockerfile: Dockerfile + args: + - PYTORCH_CUDA_VERSION=${PYTORCH_CUDA_VERSION:-cpu} + platforms: + - linux/amd64 + platform: linux/amd64 ports: - "${SPEAKER_SERVICE_PORT:-8085}:8085" environment: - MODEL_PATH=/models + - PYTORCH_CUDA_VERSION=${PYTORCH_CUDA_VERSION:-cpu} volumes: - speaker_models:/models profiles: diff --git a/docker-compose.yml b/docker-compose.yml index 3bd2e7f7..5f1ea977 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,8 +19,9 @@ include: # Core backend (always included) - # This includes: mongo, redis, qdrant, friend-backend, workers, webui - - compose/advanced-backend.yml + # This includes: friend-backend, workers, webui + - path: backends/advanced/docker-compose.yml + env_file: backends/advanced/.env # Optional services (profile-based) - compose/mycelia.yml # --profile mycelia diff --git a/scripts/configure-tailscale-serve.sh b/scripts/configure-tailscale-serve.sh new file mode 100755 index 00000000..7db13df0 --- /dev/null +++ b/scripts/configure-tailscale-serve.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# Configure Tailscale Serve for Friend-Lite +# This script sets up Tailscale serve with all required routes + +set -e + +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐ŸŒ Tailscale Serve Configuration for Friend-Lite" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" + +# Check if Tailscale is installed +if ! command -v tailscale >/dev/null 2>&1; then + echo "โŒ Tailscale not found" + echo "" + echo "๐Ÿ“ฆ Install Tailscale:" + echo " curl -fsSL https://tailscale.com/install.sh | sh" + echo " sudo tailscale up" + exit 1 +fi + +# Check if Tailscale is running +if ! tailscale status >/dev/null 2>&1; then + echo "โŒ Tailscale is not running" + echo "" + echo "๐Ÿ”ง Start Tailscale:" + echo " sudo tailscale up" + exit 1 +fi + +# Get environment name +if [ -n "$1" ]; then + ENV_NAME="$1" +else + echo "Which environment are you configuring?" + echo "" + echo "Available environments:" + ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$| |' | sed 's/^/ - /' || echo " (none found)" + echo "" + read -p "Environment name [serve]: " ENV_NAME + ENV_NAME=${ENV_NAME:-serve} +fi + +# Load environment to get PORT_OFFSET +if [ -f "environments/${ENV_NAME}.env" ]; then + echo "โœ… Loading environment: $ENV_NAME" + source "environments/${ENV_NAME}.env" + BACKEND_PORT=$((8000 + ${PORT_OFFSET:-0})) + WEBUI_PORT=$((3010 + ${PORT_OFFSET:-0})) +else + echo "โš ๏ธ Environment file not found: environments/${ENV_NAME}.env" + echo " Using default ports (no offset)" + BACKEND_PORT=8000 + WEBUI_PORT=3010 +fi + +echo "" +echo "๐Ÿ“ Configuration:" +echo " Environment: $ENV_NAME" +echo " Backend: localhost:$BACKEND_PORT" +echo " WebUI: localhost:$WEBUI_PORT" +echo "" + +# Get Tailscale hostname +TAILSCALE_HOSTNAME=$(tailscale status --json 2>/dev/null | grep -A 20 '"Self"' | grep '"DNSName"' | cut -d'"' -f4 | sed 's/\.$//') +if [ -z "$TAILSCALE_HOSTNAME" ]; then + echo "โŒ Could not detect Tailscale hostname" + exit 1 +fi + +echo " Hostname: $TAILSCALE_HOSTNAME" +echo "" + +# Confirm before proceeding +read -p "Configure Tailscale serve with these settings? (y/N): " confirm +if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + echo "โŒ Aborted" + exit 0 +fi + +echo "" +echo "๐Ÿ”ง Stopping existing Tailscale serve configuration..." +tailscale serve off 2>/dev/null || true + +echo "" +echo "๐Ÿ”ง Configuring routes..." + +# Configure backend API routes +tailscale serve --bg --set-path /api http://localhost:${BACKEND_PORT}/api 2>/dev/null +echo " โœ… /api" + +tailscale serve --bg --set-path /auth http://localhost:${BACKEND_PORT}/auth 2>/dev/null +echo " โœ… /auth" + +tailscale serve --bg --set-path /users http://localhost:${BACKEND_PORT}/users 2>/dev/null +echo " โœ… /users" + +tailscale serve --bg --set-path /docs http://localhost:${BACKEND_PORT}/docs 2>/dev/null +echo " โœ… /docs" + +tailscale serve --bg --set-path /health http://localhost:${BACKEND_PORT}/health 2>/dev/null +echo " โœ… /health" + +tailscale serve --bg --set-path /readiness http://localhost:${BACKEND_PORT}/readiness 2>/dev/null +echo " โœ… /readiness" + +tailscale serve --bg --set-path /ws_pcm http://localhost:${BACKEND_PORT}/ws_pcm 2>/dev/null +echo " โœ… /ws_pcm" + +# Configure frontend (root path last) +tailscale serve --bg http://localhost:${WEBUI_PORT} 2>/dev/null +echo " โœ… / (frontend)" + +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "โœ… Tailscale Serve Configured Successfully!" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "๐ŸŒ Your service is now accessible at:" +echo " https://${TAILSCALE_HOSTNAME}/" +echo "" +echo "๐Ÿ“‹ Current configuration:" +echo "" +tailscale serve status +echo "" +echo "๐Ÿ’ก To reconfigure for a different environment:" +echo " ./scripts/configure-tailscale-serve.sh " +echo "" +echo "๐Ÿ’ก To stop Tailscale serve:" +echo " tailscale serve off" +echo "" diff --git a/scripts/finalize-setup.sh b/scripts/finalize-setup.sh new file mode 100755 index 00000000..790dba8e --- /dev/null +++ b/scripts/finalize-setup.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Friend-Lite Setup Finalization +# This script runs at the end of the wizard to: +# 1. Generate Caddyfile for all environments +# 2. Provision Tailscale certificates if Caddy is enabled + +set -e + +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐Ÿ”ง Step 4: Finalizing Setup" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" + +# Load config to check for Caddy +if [ -f "config-docker.env" ]; then + source config-docker.env +fi + +# Check if Caddy proxy is enabled +if [ "$USE_CADDY_PROXY" = "true" ]; then + echo "๐Ÿ“ Caddy reverse proxy is enabled" + echo "" + + # Check if we have environments + if [ ! -d "environments" ] || [ -z "$(ls -A environments/*.env 2>/dev/null)" ]; then + echo "โš ๏ธ No environments found - skipping Caddyfile generation" + echo "" + else + # Generate Caddyfile for all environments + echo "๐Ÿ”ง Generating Caddyfile for all environments..." + ./scripts/generate-caddyfile.sh + echo "" + + # Provision Tailscale certificates + if [ -n "$TAILSCALE_HOSTNAME" ]; then + # Create certs directory + mkdir -p certs + + CERT_FILE="certs/${TAILSCALE_HOSTNAME}.crt" + + if [ ! -f "$CERT_FILE" ]; then + echo "๐Ÿ” Provisioning Tailscale HTTPS certificates..." + echo "" + echo " This enables HTTPS for all environment subdomains:" + for env_file in environments/*.env; do + [ -f "$env_file" ] || continue + env_name=$(basename "$env_file" .env) + echo " โ€ข https://${env_name}.${TAILSCALE_HOSTNAME}" + done + echo " โ€ข https://${TAILSCALE_HOSTNAME} (default)" + echo "" + echo " Running: tailscale cert ${TAILSCALE_HOSTNAME}" + echo "" + + if tailscale cert "${TAILSCALE_HOSTNAME}" 2>&1; then + # Move certificates to certs directory + mv "${TAILSCALE_HOSTNAME}.crt" "certs/" + mv "${TAILSCALE_HOSTNAME}.key" "certs/" + + echo "" + echo "โœ… Certificates provisioned successfully!" + echo " Location: certs/${TAILSCALE_HOSTNAME}.{crt,key}" + echo "" + else + echo "" + echo "โš ๏ธ Certificate provisioning failed." + echo "" + echo " This may happen if:" + echo " โ€ข Tailscale is not running (run: sudo tailscale up)" + echo " โ€ข You don't have permission to provision certs" + echo "" + echo " You can provision certificates manually later with:" + echo " tailscale cert ${TAILSCALE_HOSTNAME}" + echo " mv ${TAILSCALE_HOSTNAME}.* certs/" + echo "" + echo " Note: Services will not start without certificates." + echo " After provisioning, run: ./start-env.sh " + echo "" + fi + else + echo "โœ… Tailscale certificates already exist at: $CERT_FILE" + echo "" + fi + fi + fi +else + echo "โ„น๏ธ Caddy reverse proxy not enabled - skipping certificate setup" + echo "" +fi + +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "โœ… Setup finalization complete!" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" diff --git a/scripts/generate-caddyfile.sh b/scripts/generate-caddyfile.sh new file mode 100755 index 00000000..40ac8787 --- /dev/null +++ b/scripts/generate-caddyfile.sh @@ -0,0 +1,183 @@ +#!/bin/bash +# Generate Caddyfile with subdomain routing for active environments +# This script scans environments/ directory and creates routes for each + +set -e + +CADDY_DIR="caddy" +CADDYFILE="${CADDY_DIR}/Caddyfile" + +# Create caddy directory if it doesn't exist +mkdir -p "${CADDY_DIR}" + +# Get Tailscale hostname from config +if [ -f "config-docker.env" ]; then + source config-docker.env +fi + +if [ -z "$TAILSCALE_HOSTNAME" ]; then + echo "โŒ TAILSCALE_HOSTNAME not configured" + echo " Run: make setup-tailscale" + exit 1 +fi + +echo "๐Ÿ“ Generating Caddyfile for Tailscale hostname: ${TAILSCALE_HOSTNAME}" +echo "" + +# Start Caddyfile +cat > "${CADDYFILE}" << EOF +# Auto-generated Caddyfile for Friend-Lite multi-environment routing +# Generated by scripts/generate-caddyfile.sh +# Uses path-based routing: https://hostname/env/... + +# Global options +{ + # Use Tailscale HTTPS certificates + auto_https off +} + +https://${TAILSCALE_HOSTNAME} { + tls /certs/${TAILSCALE_HOSTNAME}.crt /certs/${TAILSCALE_HOSTNAME}.key + +EOF + +# Scan environments directory +if [ ! -d "environments" ]; then + echo "โš ๏ธ No environments directory found" + exit 1 +fi + +env_count=0 +for env_file in environments/*.env; do + [ -f "$env_file" ] || continue + + # Extract environment name + env_name=$(basename "$env_file" .env) + + # Load environment to get port offset and compose project name + source "$env_file" + + # Calculate ports + WEBUI_PORT=$((3010 + PORT_OFFSET)) + BACKEND_PORT=$((8000 + PORT_OFFSET)) + + # Use COMPOSE_PROJECT_NAME from environment file (or fall back to friend-lite-${env_name}) + if [ -z "$COMPOSE_PROJECT_NAME" ]; then + COMPOSE_PROJECT_NAME="friend-lite-${env_name}" + fi + + # Container names with compose project prefix + WEBUI_CONTAINER="${COMPOSE_PROJECT_NAME}-webui-1" + BACKEND_CONTAINER="${COMPOSE_PROJECT_NAME}-friend-backend-1" + + echo " Adding route: /${env_name}/*" + echo " WebUI: ${WEBUI_CONTAINER}" + echo " Backend: ${BACKEND_CONTAINER}" + + # Add path-based route for this environment + cat >> "${CADDYFILE}" << EOF + # ${env_name} environment + # Redirect exact path without trailing slash + @${env_name}_exact path /${env_name} + redir @${env_name}_exact /${env_name}/ + + # Mycelia routes (if Mycelia is enabled) + handle_path /${env_name}/mycelia/* { + # Mycelia backend API routes + @mycelia_api path /api/* /oauth/* + handle @mycelia_api { + reverse_proxy ${COMPOSE_PROJECT_NAME}-mycelia-backend-1:5100 + } + + # Mycelia frontend - serve all other paths + handle { + reverse_proxy ${COMPOSE_PROJECT_NAME}-mycelia-frontend-1:8080 { + header_up X-Forwarded-Prefix /${env_name}/mycelia + } + } + } + + # handle_path automatically strips prefix for paths with trailing slash + handle_path /${env_name}/* { + # Backend routes + @api path /api/* + @auth path /auth/* + @users path /users/* + @ws path /ws* /ws_pcm* + @health path /health /readiness + @docs path /docs* /openapi.json + + handle @api { + reverse_proxy ${BACKEND_CONTAINER}:8000 + } + handle @auth { + reverse_proxy ${BACKEND_CONTAINER}:8000 + } + handle @users { + reverse_proxy ${BACKEND_CONTAINER}:8000 + } + handle @ws { + reverse_proxy ${BACKEND_CONTAINER}:8000 + } + handle @health { + reverse_proxy ${BACKEND_CONTAINER}:8000 + } + handle @docs { + reverse_proxy ${BACKEND_CONTAINER}:8000 + } + + # WebUI - serve all other paths + handle { + reverse_proxy ${WEBUI_CONTAINER}:80 { + header_up X-Forwarded-Prefix /${env_name} + } + } + } + +EOF + + env_count=$((env_count + 1)) +done + +# Close the server block +cat >> "${CADDYFILE}" << 'EOF' + # Root path - show environment list + handle / { + respond ` + +Friend-Lite Environments + +

Friend-Lite Environments

+

Select an environment:

+
    +EOF + +# Add links to each environment +for env_file in environments/*.env; do + [ -f "$env_file" ] || continue + env_name=$(basename "$env_file" .env) + echo "
  • ${env_name}
  • " >> "${CADDYFILE}" +done + +cat >> "${CADDYFILE}" << 'EOF' +
+ + +` 200 + } +} +EOF + +echo "" +echo "โœ… Caddyfile generated: ${CADDYFILE}" +echo "๐Ÿ“Š Configured ${env_count} environment(s):" +echo "" +for env_file in environments/*.env; do + [ -f "$env_file" ] || continue + env_name=$(basename "$env_file" .env) + echo " โ€ข ${env_name}" +done +echo "" +echo "๐Ÿ’ก After starting an environment, it will be accessible at:" +echo " https://${TAILSCALE_HOSTNAME}//" +echo "" diff --git a/scripts/install-deps.sh b/scripts/install-deps.sh new file mode 100755 index 00000000..744e6f63 --- /dev/null +++ b/scripts/install-deps.sh @@ -0,0 +1,318 @@ +#!/bin/bash +set -e + +# Friend-Lite Dependency Installation Script +# Installs all required dependencies for running Friend-Lite +# Works on Ubuntu/Debian-based systems (including WSL2) + +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐Ÿ”ง Friend-Lite Dependency Installer" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "This script will install:" +echo " โ€ข Git (version control)" +echo " โ€ข Make (build automation)" +echo " โ€ข curl (HTTP client)" +echo " โ€ข Docker & Docker Compose (container platform)" +echo "" + +# Detect OS +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$ID + OS_VERSION=$VERSION_ID + else + echo "โŒ Cannot detect Linux distribution" + exit 1 + fi +elif [[ "$OSTYPE" == "darwin"* ]]; then + OS="macos" +else + echo "โŒ Unsupported operating system: $OSTYPE" + echo "This script supports Ubuntu/Debian and macOS only" + exit 1 +fi + +echo "๐Ÿ“‹ Detected OS: $OS" +echo "" + +# Check if running in WSL +if grep -qi microsoft /proc/version 2>/dev/null; then + echo "๐ŸชŸ Running in WSL (Windows Subsystem for Linux)" + IN_WSL=true +else + IN_WSL=false +fi +echo "" + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to install packages on Ubuntu/Debian +install_ubuntu_deps() { + echo "๐Ÿ“ฆ Installing dependencies for Ubuntu/Debian..." + echo "" + + # Update package lists + echo "๐Ÿ“ฅ Updating package lists..." + sudo apt-get update -qq + echo "โœ… Package lists updated" + echo "" + + # Install basic tools + echo "๐Ÿ”ง Installing basic tools (git, make, curl)..." + sudo apt-get install -y git make curl wget ca-certificates gnupg lsb-release + echo "โœ… Basic tools installed" + echo "" + + # Check if Docker is already installed + if command_exists docker; then + echo "โ„น๏ธ Docker is already installed" + docker --version + else + # Check if we're in WSL - Docker Desktop is preferred there + if [ "$IN_WSL" = true ]; then + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + echo "๐ŸชŸ WSL DETECTED - Docker Installation Options" + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + echo "" + echo "You have two options for Docker on WSL:" + echo "" + echo "1. Docker Desktop (Recommended)" + echo " โ€ข GUI application on Windows" + echo " โ€ข Easiest to manage containers" + echo " โ€ข Shared daemon between Windows and WSL" + echo " โ€ข Download: https://www.docker.com/products/docker-desktop" + echo "" + echo "2. Docker Engine in WSL (Advanced)" + echo " โ€ข Command-line only" + echo " โ€ข Lighter weight (no GUI)" + echo " โ€ข Runs entirely in WSL" + echo "" + read -p "Install Docker Engine in WSL? (y/N): " install_docker + + if [ "$install_docker" = "y" ] || [ "$install_docker" = "Y" ]; then + echo "" + echo "๐Ÿณ Installing Docker Engine in WSL..." + + # Add Docker's official GPG key + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg + + # Set up Docker repository + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + + # Install Docker Engine + sudo apt-get update -qq + sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + # Add current user to docker group + sudo usermod -aG docker $USER + + # Start Docker service + sudo service docker start + + echo "โœ… Docker Engine installed in WSL" + echo "" + echo "โš ๏ธ IMPORTANT: You need to log out and log back in for group changes to take effect" + echo " Or run: newgrp docker" + echo "" + else + echo "" + echo "โ„น๏ธ Skipping Docker installation" + echo " Please install Docker Desktop for Windows, then:" + echo " 1. Open Docker Desktop Settings" + echo " 2. Go to Resources โ†’ WSL Integration" + echo " 3. Enable integration with Ubuntu" + echo "" + fi + else + # Native Linux - install Docker Engine + echo "๐Ÿณ Installing Docker Engine..." + + # Add Docker's official GPG key + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/$OS/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg + + # Set up Docker repository + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$OS \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + + # Install Docker Engine + sudo apt-get update -qq + sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + # Add current user to docker group + sudo usermod -aG docker $USER + + # Start Docker service + sudo systemctl start docker + sudo systemctl enable docker + + echo "โœ… Docker Engine installed" + echo "" + echo "โš ๏ธ IMPORTANT: You need to log out and log back in for group changes to take effect" + echo " Or run: newgrp docker" + echo "" + fi + fi +} + +# Function to install packages on macOS +install_macos_deps() { + echo "๐Ÿ“ฆ Installing dependencies for macOS..." + echo "" + + # Check if Homebrew is installed + if ! command_exists brew; then + echo "๐Ÿบ Homebrew not found. Installing Homebrew..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + echo "โœ… Homebrew installed" + echo "" + else + echo "โœ… Homebrew is already installed" + echo "" + fi + + # Install basic tools + echo "๐Ÿ”ง Installing basic tools..." + brew install git make curl + echo "โœ… Basic tools installed" + echo "" + + # Check if Docker Desktop is installed + if command_exists docker; then + echo "โ„น๏ธ Docker is already installed" + docker --version + else + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + echo "๐Ÿณ Docker Installation Required" + echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + echo "" + echo "Docker Desktop is required for Friend-Lite on macOS." + echo "" + echo "Options:" + echo " 1. Download manually: https://www.docker.com/products/docker-desktop" + echo " 2. Install via Homebrew: brew install --cask docker" + echo "" + read -p "Install Docker Desktop via Homebrew? (y/N): " install_docker + + if [ "$install_docker" = "y" ] || [ "$install_docker" = "Y" ]; then + echo "" + echo "๐Ÿณ Installing Docker Desktop..." + brew install --cask docker + echo "" + echo "โœ… Docker Desktop installed" + echo "" + echo "โš ๏ธ IMPORTANT: Open Docker Desktop from Applications to start Docker" + else + echo "" + echo "โ„น๏ธ Please install Docker Desktop manually before continuing" + fi + fi +} + +# Install dependencies based on OS +case $OS in + ubuntu|debian) + install_ubuntu_deps + ;; + macos) + install_macos_deps + ;; + *) + echo "โŒ Unsupported OS: $OS" + echo "This script supports Ubuntu, Debian, and macOS" + exit 1 + ;; +esac + +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "โœ… Dependency Installation Complete!" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "๐Ÿ“‹ Installed tools:" +echo "" + +# Verify installations +if command_exists git; then + echo " โœ… Git: $(git --version | cut -d' ' -f3)" +else + echo " โŒ Git: Not found" +fi + +if command_exists make; then + echo " โœ… Make: $(make --version | head -n1 | cut -d' ' -f3)" +else + echo " โŒ Make: Not found" +fi + +if command_exists curl; then + echo " โœ… curl: $(curl --version | head -n1 | cut -d' ' -f2)" +else + echo " โŒ curl: Not found" +fi + +if command_exists docker; then + echo " โœ… Docker: $(docker --version | cut -d' ' -f3 | tr -d ',')" +else + echo " โŒ Docker: Not found" +fi + +if command_exists docker && docker compose version >/dev/null 2>&1; then + echo " โœ… Docker Compose: $(docker compose version | cut -d' ' -f4)" +else + echo " โŒ Docker Compose: Not found" +fi + +echo "" + +# Check if Docker is accessible +if command_exists docker; then + if docker ps >/dev/null 2>&1; then + echo "โœ… Docker is running and accessible" + else + echo "โš ๏ธ Docker is installed but not accessible" + echo "" + if [ "$IN_WSL" = true ]; then + echo "๐Ÿ’ก If you installed Docker Desktop on Windows:" + echo " 1. Make sure Docker Desktop is running" + echo " 2. Open Docker Desktop Settings โ†’ Resources โ†’ WSL Integration" + echo " 3. Enable Ubuntu-22.04" + echo " 4. Click 'Apply & Restart'" + else + echo "๐Ÿ’ก You may need to:" + echo " 1. Log out and log back in (for group permissions)" + echo " 2. Or run: newgrp docker" + echo " 3. Or start Docker: sudo systemctl start docker" + fi + fi +else + echo "โš ๏ธ Docker is not installed" + echo "" + if [ "$IN_WSL" = true ]; then + echo "๐Ÿ’ก For WSL, we recommend Docker Desktop for Windows" + echo " Download: https://www.docker.com/products/docker-desktop" + fi +fi + +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐Ÿš€ Next Steps" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "Run the Friend-Lite setup wizard:" +echo " make wizard" +echo "" +echo "Or set up components individually:" +echo " make setup-secrets # Configure API keys" +echo " make setup-environment # Create environment" +echo " ./start-env.sh dev # Start Friend-Lite" +echo "" diff --git a/scripts/setup-environment.sh b/scripts/setup-environment.sh new file mode 100755 index 00000000..479aff0e --- /dev/null +++ b/scripts/setup-environment.sh @@ -0,0 +1,286 @@ +#!/bin/bash +set -e + +# Friend-Lite Environment Setup Script +# This script creates custom environment configurations + +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐Ÿ“ฆ Step 1: Environment Setup" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "Environments allow you to:" +echo " โ€ข Run multiple isolated instances (dev, staging, prod)" +echo " โ€ข Use different databases and ports for each" +echo " โ€ข Test changes without affecting production" +echo "" + +# Check existing environments +if [ -d "environments" ] && [ -n "$(ls -A environments/*.env 2>/dev/null)" ]; then + echo "๐Ÿ“‹ Existing environments:" + ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$||' | sed 's/^/ - /' + echo "" +fi + +# Get user name for personalized configuration +echo "๐Ÿ‘ค User Information" +echo "" +echo "This name will be used for the memory system and personalization" +echo "" +read -p "Your name [user]: " user_name +user_name=${user_name:-user} +echo "" + +# Get environment name +read -p "Environment name [dev]: " env_name +env_name=${env_name:-dev} +mkdir -p environments +env_file="environments/$env_name.env" +echo "" + +if [ -f "$env_file" ]; then + echo "โš ๏ธ Environment '$env_name' already exists" + read -p "Do you want to overwrite it? (y/N): " overwrite + if [ "$overwrite" != "y" ] && [ "$overwrite" != "Y" ]; then + echo "" + echo "โ„น๏ธ Keeping existing environment" + exit 0 + fi + echo "" + cp "$env_file" "$env_file.backup.$(date +%Y%m%d_%H%M%S)" + echo "๐Ÿ“ Backed up existing environment" + echo "" +fi + +# Get port offset +echo "๐Ÿ”ข Port Configuration" +echo "" +echo "Each environment needs a unique port offset to avoid conflicts." +echo "" +echo "Port offset applies ONLY to API and WebUI containers:" +echo " dev: 0 (Backend: 8000, WebUI: 3010)" +echo " staging: 100 (Backend: 8100, WebUI: 3110)" +echo " prod: 200 (Backend: 8200, WebUI: 3210)" +echo "" +echo "Note: Infrastructure services (MongoDB:27017, Redis:6379, Qdrant:6033/6034)" +echo " use fixed ports and are shared across all environments." +echo "" +read -p "Port offset [0]: " port_offset +port_offset=${port_offset:-0} +echo "" + +# Memory Backend Selection +echo "๐Ÿง  Memory Backend" +echo "" +echo "Choose your memory backend:" +echo " 1) OpenMemory (recommended - MCP server with advanced features)" +echo " 2) Friend-Lite Standard (built-in memory system)" +echo " 3) Mycelia (experimental - graph-based memory)" +echo "" +read -p "Memory backend (1-3) [1]: " memory_choice +memory_choice=${memory_choice:-1} +echo "" + +case $memory_choice in + 1) + memory_provider="openmemory_mcp" + echo "โœ… Using OpenMemory MCP backend" + echo "" + + # Ask about Neo4j graph memory + echo "๐Ÿ”— Neo4j Graph Memory (Optional)" + echo "" + echo "Neo4j enables advanced graph-based memory relationships." + echo "This allows OpenMemory to store and query complex connections" + echo "between memories, entities, and concepts." + echo "" + read -p "Enable Neo4j graph memory? (y/N): " enable_neo4j + ;; + 2) + memory_provider="friend_lite" + echo "โœ… Using Friend-Lite standard backend" + ;; + 3) + memory_provider="mycelia" + echo "โœ… Using Mycelia backend" + ;; + *) + memory_provider="openmemory_mcp" + echo "โš ๏ธ Invalid choice, defaulting to OpenMemory" + + # Ask about Neo4j for default case too + echo "" + echo "๐Ÿ”— Neo4j Graph Memory (Optional)" + echo "" + echo "Neo4j enables advanced graph-based memory relationships." + echo "This allows OpenMemory to store and query complex connections" + echo "between memories, entities, and concepts." + echo "" + read -p "Enable Neo4j graph memory? (y/N): " enable_neo4j + ;; +esac +echo "" + +# Optional services +echo "๐Ÿ”Œ Optional Services" +echo "" +read -p "Enable Speaker Recognition? (y/N): " enable_speaker +read -p "Enable Parakeet ASR? (y/N): " enable_parakeet +echo "" + +# Auto-enable services based on memory choice +if [ "$memory_provider" = "mycelia" ]; then + enable_mycelia="y" + echo "โœ… Mycelia service will be enabled (required for Mycelia backend)" + echo "" +elif [ "$memory_provider" = "openmemory_mcp" ]; then + enable_openmemory="y" + echo "โœ… OpenMemory MCP service will be enabled" + + # Handle Neo4j enablement + if [ "$enable_neo4j" = "y" ] || [ "$enable_neo4j" = "Y" ]; then + echo "โœ… Neo4j graph memory will be enabled" + fi + echo "" +fi + +# Find an unused Redis database number (0-15) +find_unused_redis_db() { + local used_dbs=$(grep -h "^REDIS_DATABASE=" environments/*.env 2>/dev/null | cut -d= -f2 | sort -n) + for db in {0..15}; do + if ! echo "$used_dbs" | grep -q "^${db}$"; then + echo "$db" + return + fi + done + # Fallback to 0 if all are used (shouldn't happen with 16 databases) + echo "0" +} + +redis_db=$(find_unused_redis_db) + +# Get database names +echo "๐Ÿ’พ Database Configuration" +echo "" +read -p "MongoDB database name [chronicle-$env_name]: " mongodb_db +mongodb_db=${mongodb_db:-chronicle-$env_name} +echo " Redis database: $redis_db (auto-assigned)" + +# Only ask for Mycelia database if Mycelia is enabled +if [ "$enable_mycelia" = "y" ] || [ "$enable_mycelia" = "Y" ]; then + read -p "Mycelia database name [mycelia-$env_name]: " mycelia_db + mycelia_db=${mycelia_db:-mycelia-$env_name} +else + mycelia_db="mycelia-$env_name" +fi + +# Only ask for OpenMemory database if OpenMemory is enabled +if [ "$enable_openmemory" = "y" ] || [ "$enable_openmemory" = "Y" ]; then + read -p "OpenMemory database name [openmemory-$env_name]: " openmemory_db + openmemory_db=${openmemory_db:-openmemory-$env_name} +else + openmemory_db="openmemory-$env_name" +fi +echo "" + +# If speaker recognition or ASR is enabled, ask about GPU support +pytorch_cuda_version="cpu" +if [ "$enable_speaker" = "y" ] || [ "$enable_speaker" = "Y" ] || [ "$enable_parakeet" = "y" ] || [ "$enable_parakeet" = "Y" ]; then + echo "๐ŸŽค GPU Configuration (for Speaker Recognition / ASR)" + echo "" + echo " GPU Support:" + echo " โ€ข cpu - CPU only (slower, works everywhere)" + echo " โ€ข cu121 - CUDA 12.1 (NVIDIA GPU)" + echo " โ€ข cu126 - CUDA 12.6 (NVIDIA GPU)" + echo " โ€ข cu128 - CUDA 12.8 (Latest NVIDIA GPU)" + read -p " PyTorch version [cpu]: " pytorch_input + pytorch_cuda_version=${pytorch_input:-cpu} + echo "" +fi + +services="" +if [ "$enable_mycelia" = "y" ] || [ "$enable_mycelia" = "Y" ]; then + services="${services:+$services }mycelia" +fi +if [ "$enable_speaker" = "y" ] || [ "$enable_speaker" = "Y" ]; then + services="${services:+$services }speaker" +fi +if [ "$enable_openmemory" = "y" ] || [ "$enable_openmemory" = "Y" ]; then + services="${services:+$services }openmemory" +fi +if [ "$enable_parakeet" = "y" ] || [ "$enable_parakeet" = "Y" ]; then + services="${services:+$services }asr" +fi +# Note: Neo4j is infrastructure, not a profile service +echo "" + +# Write environment file +echo "๐Ÿ“ Creating environment file: $env_file" +echo "" + +{ + printf "# ========================================\n" + printf "# Friend-Lite Environment: %s\n" "$env_name" + printf "# ========================================\n" + printf "# Generated: %s\n" "$(date)" + printf "\n" + printf "# Environment identification\n" + printf "ENV_NAME=%s\n" "$env_name" + printf "COMPOSE_PROJECT_NAME=chronicle-%s\n" "$env_name" + printf "\n" + printf "# Port offset (each environment needs unique ports)\n" + printf "PORT_OFFSET=%s\n" "$port_offset" + printf "\n" + printf "# Data directory (isolated per environment)\n" + printf "DATA_DIR=./data/%s\n" "$env_name" + printf "\n" + printf "# Database names (isolated per environment)\n" + printf "MONGODB_DATABASE=%s\n" "$mongodb_db" + printf "REDIS_DATABASE=%s\n" "$redis_db" + printf "MYCELIA_DB=%s\n" "$mycelia_db" + printf "OPENMEMORY_DB=%s\n" "$openmemory_db" + printf "\n" + printf "# Memory Backend\n" + printf "MEMORY_PROVIDER=%s\n" "$memory_provider" + printf "\n" + printf "# OpenMemory User Configuration\n" + printf "OPENMEMORY_USER_ID=%s\n" "$user_name" + printf "\n" + printf "# Neo4j Graph Memory (for OpenMemory)\n" + if [ "$enable_neo4j" = "y" ] || [ "$enable_neo4j" = "Y" ]; then + printf "NEO4J_ENABLED=true\n" + else + printf "NEO4J_ENABLED=false\n" + fi + printf "\n" + printf "# Optional services\n" + printf "SERVICES=\"%s\"\n" "$services" + printf "\n" + printf "# Speaker Recognition PyTorch version\n" + printf "PYTORCH_CUDA_VERSION=%s\n" "$pytorch_cuda_version" + printf "\n" + + # Add Vite base path for Caddy path-based routing + if [ -f "config-docker.env" ] && grep -q "USE_CADDY_PROXY=true" config-docker.env; then + printf "# WebUI base path for Caddy reverse proxy\n" + printf "VITE_BASE_PATH=/%s/\n" "$env_name" + printf "\n" + fi +} > "$env_file" + +echo "โœ… Environment created: $env_name" +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "โœ… Environment setup complete!" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "๐Ÿ“„ Environment file: $env_file" +echo "๐Ÿ‘ค User: $user_name" +if [ -n "$services" ]; then + echo "๐Ÿ”Œ Configured services: $services" +fi +echo "" +echo "๐Ÿš€ Start this environment with:" +echo " ./start-env.sh $env_name" +echo "" +echo "๐Ÿ’ก Your selected services will start automatically!" +echo "" diff --git a/scripts/setup-secrets.sh b/scripts/setup-secrets.sh new file mode 100755 index 00000000..e8c01def --- /dev/null +++ b/scripts/setup-secrets.sh @@ -0,0 +1,136 @@ +#!/bin/bash +set -e + +# Friend-Lite Secrets Configuration Script +# This script handles interactive configuration of API keys and passwords + +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐Ÿ” Step 1: Secrets Configuration" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" + +# Check if .env.secrets exists +if [ -f ".env.secrets" ]; then + echo "โ„น๏ธ .env.secrets already exists" + echo "" + read -p "Do you want to reconfigure it? (y/N): " reconfigure + + if [ "$reconfigure" != "y" ] && [ "$reconfigure" != "Y" ]; then + echo "" + echo "โœ… Keeping existing secrets" + exit 0 + fi + + echo "" + echo "๐Ÿ“ Backing up existing .env.secrets..." + cp .env.secrets .env.secrets.backup.$(date +%Y%m%d_%H%M%S) + echo "" +else + echo "๐Ÿ“ Creating .env.secrets from template..." + cp .env.secrets.template .env.secrets + echo "โœ… Created .env.secrets" + echo "" +fi + +echo "๐Ÿ”‘ Required Secrets Configuration" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "Let's configure your secrets. Press Enter to skip optional ones." +echo "" + +# JWT Secret Key (required) +echo "1๏ธโƒฃ JWT Secret Key (required for authentication)" +echo " This is used to sign JWT tokens. Should be random and secure." +read -p " Enter JWT secret key (or press Enter to generate): " jwt_key + +if [ -z "$jwt_key" ]; then + jwt_key=$(openssl rand -hex 32 2>/dev/null || cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 64 | head -n 1) + echo " โœ… Generated random key: $jwt_key" +fi + +sed -i.bak "s|^AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=$jwt_key|" .env.secrets && rm .env.secrets.bak +echo "" + +# Admin credentials +echo "2๏ธโƒฃ Admin Account" +read -p " Admin email (default: admin@example.com): " admin_email +admin_email=${admin_email:-admin@example.com} +sed -i.bak "s|^ADMIN_EMAIL=.*|ADMIN_EMAIL=$admin_email|" .env.secrets && rm .env.secrets.bak + +read -sp " Admin password: " admin_pass +echo "" +if [ -n "$admin_pass" ]; then + sed -i.bak "s|^ADMIN_PASSWORD=.*|ADMIN_PASSWORD=$admin_pass|" .env.secrets && rm .env.secrets.bak +fi +echo "" + +# OpenAI API Key +echo "3๏ธโƒฃ OpenAI API Key (required for memory extraction)" +echo " Get your key from: https://platform.openai.com/api-keys" +read -p " OpenAI API key (or press Enter to skip): " openai_key + +if [ -n "$openai_key" ]; then + # Strip any leading = or whitespace + openai_key=$(echo "$openai_key" | sed 's/^[[:space:]]*=*//') + sed -i.bak "s|^OPENAI_API_KEY=.*|OPENAI_API_KEY=$openai_key|" .env.secrets && rm .env.secrets.bak +fi +echo "" + +# Deepgram API Key +echo "4๏ธโƒฃ Deepgram API Key (recommended for transcription)" +echo " Get your key from: https://console.deepgram.com/" +read -p " Deepgram API key (or press Enter to skip): " deepgram_key + +if [ -n "$deepgram_key" ]; then + # Strip any leading = or whitespace + deepgram_key=$(echo "$deepgram_key" | sed 's/^[[:space:]]*=*//') + sed -i.bak "s|^DEEPGRAM_API_KEY=.*|DEEPGRAM_API_KEY=$deepgram_key|" .env.secrets && rm .env.secrets.bak +fi +echo "" + +# Mistral API Key (optional) +echo "5๏ธโƒฃ Mistral API Key (optional - alternative transcription)" +echo " Get your key from: https://console.mistral.ai/" +read -p " Mistral API key (or press Enter to skip): " mistral_key + +if [ -n "$mistral_key" ]; then + # Strip any leading = or whitespace + mistral_key=$(echo "$mistral_key" | sed 's/^[[:space:]]*=*//') + sed -i.bak "s|^MISTRAL_API_KEY=.*|MISTRAL_API_KEY=$mistral_key|" .env.secrets && rm .env.secrets.bak +fi +echo "" + +# Hugging Face Token (optional) +echo "6๏ธโƒฃ Hugging Face Token (optional - for speaker recognition models)" +echo " Get your token from: https://huggingface.co/settings/tokens" +read -p " HF token (or press Enter to skip): " hf_token + +if [ -n "$hf_token" ]; then + # Strip any leading = or whitespace + hf_token=$(echo "$hf_token" | sed 's/^[[:space:]]*=*//') + sed -i.bak "s|^HF_TOKEN=.*|HF_TOKEN=$hf_token|" .env.secrets && rm .env.secrets.bak +fi +echo "" + +# Neo4j Password (optional) +echo "7๏ธโƒฃ Neo4j Password (optional - for OpenMemory graph memory)" +echo " If you're using OpenMemory with Neo4j graph memory, set a secure password." +echo " This is required if you enabled Neo4j in your environment setup." +read -sp " Neo4j password (or press Enter to skip): " neo4j_pass +echo "" + +if [ -n "$neo4j_pass" ]; then + sed -i.bak "s|^NEO4J_PASSWORD=.*|NEO4J_PASSWORD=$neo4j_pass|" .env.secrets && rm .env.secrets.bak + echo " โœ… Neo4j password set" +else + echo " โš ๏ธ Skipped - using default password (not recommended for production)" +fi +echo "" + +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "โœ… Secrets configured successfully!" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "๐Ÿ“„ Configuration saved to: .env.secrets" +echo "๐Ÿ”’ This file is gitignored and will not be committed" +echo "" diff --git a/scripts/setup-tailscale.sh b/scripts/setup-tailscale.sh new file mode 100755 index 00000000..8192c2cd --- /dev/null +++ b/scripts/setup-tailscale.sh @@ -0,0 +1,295 @@ +#!/bin/bash +set -e + +# Friend-Lite Tailscale Configuration Script +# This script handles interactive Tailscale setup for distributed deployment + +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐ŸŒ Step 3: Tailscale Configuration (Optional)" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "Tailscale enables secure distributed deployments:" +echo " โ€ข Run services on different machines" +echo " โ€ข Secure service-to-service communication" +echo " โ€ข Access from mobile devices" +echo " โ€ข Automatic HTTPS with 'tailscale serve'" +echo "" + +read -p "Do you want to configure Tailscale? (y/N): " use_tailscale +if [ "$use_tailscale" != "y" ] && [ "$use_tailscale" != "Y" ]; then + echo "" + echo "โ„น๏ธ Skipping Tailscale setup" + echo " You can run this later with: make setup-tailscale" + exit 0 +fi + +echo "" + +# Check if Tailscale is installed +if ! command -v tailscale >/dev/null 2>&1; then + echo "โŒ Tailscale not found" + echo "" + echo "๐Ÿ“ฆ Install Tailscale:" + echo " curl -fsSL https://tailscale.com/install.sh | sh" + echo " sudo tailscale up" + echo "" + echo "Then run this setup again: make setup-tailscale" + exit 1 +fi + +echo "โœ… Tailscale is installed" +echo "" + +# Get Tailscale status +echo "๐Ÿ“Š Checking Tailscale status..." +if ! tailscale status >/dev/null 2>&1; then + echo "โŒ Tailscale is not running" + echo "" + echo "๐Ÿ”ง Start Tailscale:" + echo " sudo tailscale up" + echo "" + exit 1 +fi + +echo "โœ… Tailscale is running" +echo "" +echo "๐Ÿ“‹ Your Tailscale devices:" +echo "" +tailscale status | head -n 10 +echo "" + +# Get Tailscale hostname +echo "๐Ÿท๏ธ Tailscale Hostname Configuration" +echo "" +echo "Your Tailscale hostname is the DNS name assigned to THIS machine." +echo "It's different from the IP address - it's a permanent name." +echo "" +echo "๐Ÿ“‹ To find your Tailscale hostname:" +echo " 1. Run: tailscale status" +echo " 2. Look for this machine's name in the first column" +echo " 3. The full hostname is shown on the right (ends in .ts.net)" +echo "" +echo "Example output:" +echo " anubis 100.x.x.x anubis.tail12345.ts.net <-- Your hostname" +echo "" + +default_hostname=$(tailscale status --json 2>/dev/null | grep -A 20 '"Self"' | grep '"DNSName"' | cut -d'"' -f4 | sed 's/\.$//') +if [ -n "$default_hostname" ]; then + echo "๐Ÿ’ก Auto-detected hostname for THIS machine: $default_hostname" + echo "" +fi + +read -p "Tailscale hostname [$default_hostname]: " tailscale_hostname +tailscale_hostname=${tailscale_hostname:-$default_hostname} + +if [ -z "$tailscale_hostname" ]; then + echo "" + echo "โŒ No hostname provided" + exit 1 +fi + +export TAILSCALE_HOSTNAME=$tailscale_hostname +echo "" +echo "โœ… Using Tailscale hostname: $tailscale_hostname" +echo "" + +# Save Tailscale hostname to config-docker.env +echo "๐Ÿ’พ Saving Tailscale hostname to config-docker.env..." +if [ -f "config-docker.env" ]; then + # Update existing TAILSCALE_HOSTNAME value + if grep -q "^TAILSCALE_HOSTNAME=" config-docker.env; then + # Replace existing value (handles both empty and non-empty) + sed -i.bak "s|^TAILSCALE_HOSTNAME=.*|TAILSCALE_HOSTNAME=$tailscale_hostname|" config-docker.env + rm -f config-docker.env.bak + else + # Append if not found + echo "TAILSCALE_HOSTNAME=$tailscale_hostname" >> config-docker.env + fi + echo "โœ… Updated config-docker.env" +else + echo "โš ๏ธ config-docker.env not found - skipping save" +fi +echo "" + +# SSL Setup +echo "๐Ÿ” SSL Certificate Configuration" +echo "" +echo "How do you want to handle HTTPS?" +echo "" +echo " 1) Use 'tailscale serve' (automatic HTTPS)" +echo " โ†’ Use this if you will only have a single environment" +echo "" +echo " 2) Use Caddy reverse proxy (automatic HTTPS, multiple environments)" +echo " โ†’ Subdomain routing: dev.${tailscale_hostname}, prod.${tailscale_hostname}" +echo " โ†’ Recommended for running multiple environments simultaneously" +echo "" +echo " 3) HTTP only (no SSL)" +echo " โ†’ Direct port access, no HTTPS" +echo "" + +read -p "Choose option (1-3) [2]: " ssl_choice +ssl_choice=${ssl_choice:-2} + +case $ssl_choice in + 1) + echo "" + echo "โœ… Will use 'tailscale serve' for automatic HTTPS" + echo "" + + # Auto-detect most recently created environment + LATEST_ENV_FILE=$(ls -t environments/*.env 2>/dev/null | head -1) + if [ -n "$LATEST_ENV_FILE" ]; then + serve_env=$(basename "$LATEST_ENV_FILE" .env) + echo "๐Ÿ“‹ Using environment: $serve_env" + echo "" + else + echo "โš ๏ธ No environments found" + read -p "Environment name [serve]: " serve_env + serve_env=${serve_env:-serve} + fi + + # Calculate ports based on environment + if [ -f "environments/${serve_env}.env" ]; then + # Source the environment file to get PORT_OFFSET + source "environments/${serve_env}.env" + BACKEND_PORT=$((8000 + ${PORT_OFFSET:-0})) + WEBUI_PORT=$((3010 + ${PORT_OFFSET:-0})) + else + # Use defaults if environment doesn't exist yet + echo "โš ๏ธ Environment file not found: environments/${serve_env}.env" + echo " Using default ports" + BACKEND_PORT=8000 + WEBUI_PORT=3010 + fi + + echo "" + echo "๐Ÿ“ Configuring tailscale serve automatically..." + echo "" + echo " Using ports:" + echo " โ€ข Backend: $BACKEND_PORT" + echo " โ€ข WebUI: $WEBUI_PORT" + echo "" + + # Stop any existing serve configuration + tailscale serve off 2>/dev/null || true + + # Configure all routes + echo " Setting up routes..." + tailscale serve --bg --set-path /api http://localhost:${BACKEND_PORT}/api + tailscale serve --bg --set-path /auth http://localhost:${BACKEND_PORT}/auth + tailscale serve --bg --set-path /users http://localhost:${BACKEND_PORT}/users + tailscale serve --bg --set-path /docs http://localhost:${BACKEND_PORT}/docs + tailscale serve --bg --set-path /health http://localhost:${BACKEND_PORT}/health + tailscale serve --bg --set-path /readiness http://localhost:${BACKEND_PORT}/readiness + tailscale serve --bg --set-path /ws_pcm http://localhost:${BACKEND_PORT}/ws_pcm + tailscale serve --bg http://localhost:${WEBUI_PORT} # Root path (frontend) + + echo "" + echo "โœ… Tailscale serve configured!" + echo "" + echo "๐Ÿ“‹ Your service is now accessible at:" + echo " https://${tailscale_hostname}/" + echo "" + echo "๐Ÿ“ Configured routes:" + tailscale serve status + echo "" + + export HTTPS_ENABLED=true + # Save to config-docker.env and disable Caddy + if [ -f "config-docker.env" ]; then + if grep -q "^HTTPS_ENABLED=" config-docker.env; then + sed -i.bak "s|^HTTPS_ENABLED=.*|HTTPS_ENABLED=true|" config-docker.env + rm -f config-docker.env.bak + else + echo "HTTPS_ENABLED=true" >> config-docker.env + fi + + # Disable Caddy proxy when using tailscale serve + if grep -q "^USE_CADDY_PROXY=" config-docker.env; then + sed -i.bak "s|^USE_CADDY_PROXY=.*|USE_CADDY_PROXY=false|" config-docker.env + rm -f config-docker.env.bak + else + echo "USE_CADDY_PROXY=false" >> config-docker.env + fi + fi + + # Remove VITE_BASE_PATH from the environment file (not needed for tailscale serve) + if [ -n "$serve_env" ] && [ -f "environments/${serve_env}.env" ]; then + if grep -q "^VITE_BASE_PATH=" "environments/${serve_env}.env"; then + sed -i.bak '/^VITE_BASE_PATH=/d' "environments/${serve_env}.env" + rm -f "environments/${serve_env}.env.bak" + + # Add comment explaining root path deployment + if ! grep -q "WebUI base path for root deployment" "environments/${serve_env}.env"; then + echo "" >> "environments/${serve_env}.env" + echo "# WebUI base path for root deployment (no Caddy, direct Tailscale serve)" >> "environments/${serve_env}.env" + echo "VITE_BASE_PATH=/" >> "environments/${serve_env}.env" + fi + fi + fi + ;; + 2) + echo "" + echo "โœ… Caddy reverse proxy with subdomain routing" + echo "" + echo "๐Ÿ“ Your environments will be accessible at:" + echo " https://dev.${tailscale_hostname}/ (dev environment)" + echo " https://test.${tailscale_hostname}/ (test environment)" + echo " https://prod.${tailscale_hostname}/ (prod environment)" + echo "" + echo "โ„น๏ธ Caddy will:" + echo " โ€ข Automatically handle HTTPS certificates via Tailscale" + echo " โ€ข Route to the correct environment based on subdomain" + echo " โ€ข Enable microphone access (requires HTTPS)" + echo " โ€ข Support multiple environments simultaneously" + echo "" + echo "๐Ÿ“‹ After starting services, you need to configure Tailscale:" + echo " tailscale serve https:443 http://localhost:443" + echo "" + + export HTTPS_ENABLED=true + export USE_CADDY_PROXY=true + + # Save to config-docker.env + if [ -f "config-docker.env" ]; then + if grep -q "^HTTPS_ENABLED=" config-docker.env; then + sed -i.bak "s|^HTTPS_ENABLED=.*|HTTPS_ENABLED=true|" config-docker.env + rm -f config-docker.env.bak + else + echo "HTTPS_ENABLED=true" >> config-docker.env + fi + + if grep -q "^USE_CADDY_PROXY=" config-docker.env; then + sed -i.bak "s|^USE_CADDY_PROXY=.*|USE_CADDY_PROXY=true|" config-docker.env + rm -f config-docker.env.bak + else + echo "USE_CADDY_PROXY=true" >> config-docker.env + fi + fi + ;; + 3) + echo "" + echo "โ„น๏ธ Skipping SSL setup" + export HTTPS_ENABLED=false + # Save to config-docker.env + if [ -f "config-docker.env" ]; then + if grep -q "^HTTPS_ENABLED=" config-docker.env; then + sed -i.bak "s|^HTTPS_ENABLED=.*|HTTPS_ENABLED=false|" config-docker.env + rm -f config-docker.env.bak + else + echo "HTTPS_ENABLED=false" >> config-docker.env + fi + fi + ;; + *) + echo "" + echo "โŒ Invalid choice" + exit 1 + ;; +esac + +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "โœ… Tailscale configuration complete!" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" diff --git a/start-env.sh b/start-env.sh index deaaf6cc..cb9ef0a3 100755 --- a/start-env.sh +++ b/start-env.sh @@ -4,6 +4,7 @@ # # Usage: # ./start-env.sh dev # Start dev environment +# ./start-env.sh dev -f # Force recreate containers # ./start-env.sh feature-123 # Start feature branch # ./start-env.sh test # Start test environment # ./start-env.sh dev --profile mycelia # With additional profiles @@ -12,22 +13,41 @@ set -e # Check if environment name provided if [ -z "$1" ]; then - echo "Usage: $0 [docker-compose-options]" + echo "Usage: $0 [options] [docker-compose-options]" echo "" echo "Available environments:" ls -1 environments/*.env 2>/dev/null | sed 's|environments/||;s|.env$| |' | sed 's/^/ - /' echo "" + echo "Options:" + echo " -f, --force Force recreate containers (useful after config changes)" + echo "" echo "Examples:" - echo " $0 dev # Start dev environment" - echo " $0 feature-123 # Start feature branch" - echo " $0 test # Start test environment" - echo " $0 dev --profile mycelia # Dev with mycelia" + echo " $0 dev # Start dev environment" + echo " $0 dev -f # Force recreate dev containers" + echo " $0 feature-123 # Start feature branch" + echo " $0 test # Start test environment" + echo " $0 dev --profile mycelia # Dev with mycelia" exit 1 fi ENV_NAME="$1" shift # Remove environment name from args +# Parse flags +FORCE_RECREATE=false +while [[ $# -gt 0 ]]; do + case $1 in + -f|--force) + FORCE_RECREATE=true + shift + ;; + *) + # Not our flag, pass through to docker compose + break + ;; + esac +done + ENV_FILE="environments/${ENV_NAME}.env" # Check if environment file exists @@ -55,6 +75,19 @@ fi # Load secrets (gitignored) if [ -f ".env.secrets" ]; then source .env.secrets + # Export secrets for docker-compose to access + export AUTH_SECRET_KEY + export ADMIN_EMAIL + export ADMIN_PASSWORD + export OPENAI_API_KEY + export DEEPGRAM_API_KEY + export MISTRAL_API_KEY + export GROQ_API_KEY + export HF_TOKEN + export LANGFUSE_PUBLIC_KEY + export LANGFUSE_SECRET_KEY + export NGROK_AUTHTOKEN + export NEO4J_PASSWORD else echo "โš ๏ธ Warning: .env.secrets not found" echo " Copy .env.secrets.template to .env.secrets and fill in your credentials" @@ -65,18 +98,68 @@ fi source "$ENV_FILE" # Calculate actual ports based on offset +# Infrastructure ports (MongoDB, Redis, Qdrant) are NOT offset - they're shared across environments +# Only API and WebUI ports get the offset for multi-environment support BACKEND_PORT=$((8000 + PORT_OFFSET)) WEBUI_PORT=$((3010 + PORT_OFFSET)) -MONGO_PORT=$((27017 + PORT_OFFSET)) -REDIS_PORT=$((6379 + PORT_OFFSET)) -QDRANT_GRPC_PORT=$((6033 + PORT_OFFSET)) -QDRANT_HTTP_PORT=$((6034 + PORT_OFFSET)) MYCELIA_BACKEND_PORT=$((5100 + PORT_OFFSET)) MYCELIA_FRONTEND_PORT=$((3003 + PORT_OFFSET)) SPEAKER_PORT=$((8085 + PORT_OFFSET)) OPENMEMORY_PORT=$((8765 + PORT_OFFSET)) +OPENMEMORY_UI_PORT=$((3330 + PORT_OFFSET)) PARAKEET_PORT=$((8767 + PORT_OFFSET)) +# Infrastructure ports - no offset (shared across environments) +MONGO_PORT=27017 +REDIS_PORT=6379 +QDRANT_GRPC_PORT=6033 +QDRANT_HTTP_PORT=6034 +NEO4J_HTTP_PORT=7474 +NEO4J_BOLT_PORT=7687 + +# Calculate VITE_BACKEND_URL based on Caddy/Tailscale configuration +if [ "$USE_CADDY_PROXY" = "true" ]; then + # Use relative URLs for same-origin requests (Caddy handles routing) + VITE_BACKEND_URL="" + echo "๐Ÿ”„ Using Caddy reverse proxy - frontend will use relative URLs" +elif [ -n "$TAILSCALE_HOSTNAME" ]; then + # Direct Tailscale access with ports + if [ "$HTTPS_ENABLED" = "true" ]; then + VITE_BACKEND_URL="https://${TAILSCALE_HOSTNAME}:${BACKEND_PORT}" + else + VITE_BACKEND_URL="http://${TAILSCALE_HOSTNAME}:${BACKEND_PORT}" + fi +else + # Use localhost for local-only development + VITE_BACKEND_URL="http://localhost:${BACKEND_PORT}" +fi + +# Calculate CORS_ORIGINS based on Caddy/Tailscale configuration +if [ "$USE_CADDY_PROXY" = "true" ]; then + # With Caddy, same-origin requests mean minimal CORS needed + # Add Tailscale hostname and common subdomains + CORS_ORIGINS="https://${TAILSCALE_HOSTNAME}" + CORS_ORIGINS="${CORS_ORIGINS},https://dev.${TAILSCALE_HOSTNAME}" + CORS_ORIGINS="${CORS_ORIGINS},https://test.${TAILSCALE_HOSTNAME}" + CORS_ORIGINS="${CORS_ORIGINS},https://prod.${TAILSCALE_HOSTNAME}" + # Add localhost for development + CORS_ORIGINS="${CORS_ORIGINS},http://localhost:${WEBUI_PORT},http://localhost:5173,http://localhost:${BACKEND_PORT}" + echo "๐ŸŒ CORS configured for Caddy subdomain routing" +else + # Standard CORS for direct access + CORS_ORIGINS="http://localhost:${WEBUI_PORT},http://localhost:5173,http://localhost:${BACKEND_PORT}" + + # Add Tailscale URLs to CORS if Tailscale hostname is configured + if [ -n "$TAILSCALE_HOSTNAME" ]; then + echo "๐ŸŒ Adding Tailscale URLs to CORS_ORIGINS..." + # Add both HTTP and HTTPS URLs for the Tailscale hostname with ports + CORS_ORIGINS="${CORS_ORIGINS},http://${TAILSCALE_HOSTNAME}:${BACKEND_PORT},https://${TAILSCALE_HOSTNAME}:${BACKEND_PORT}" + CORS_ORIGINS="${CORS_ORIGINS},http://${TAILSCALE_HOSTNAME}:${WEBUI_PORT},https://${TAILSCALE_HOSTNAME}:${WEBUI_PORT}" + # Also add URLs without ports (for tailscale serve) + CORS_ORIGINS="${CORS_ORIGINS},http://${TAILSCALE_HOSTNAME},https://${TAILSCALE_HOSTNAME}" + fi +fi + # Export all variables for docker compose export ENV_NAME export BACKEND_PORT @@ -89,12 +172,141 @@ export MYCELIA_BACKEND_PORT export MYCELIA_FRONTEND_PORT export SPEAKER_PORT export OPENMEMORY_PORT +export OPENMEMORY_UI_PORT +export OPENMEMORY_USER_ID +export OPENMEMORY_DB export PARAKEET_PORT export MONGODB_DATABASE export MYCELIA_DB +export NEO4J_HTTP_PORT +export NEO4J_BOLT_PORT export QDRANT_DATA_PATH="${DATA_DIR}/qdrant_data" export REDIS_DATA_PATH="${DATA_DIR}/redis_data" export COMPOSE_PROJECT_NAME +export VITE_BACKEND_URL +export VITE_BASE_PATH +export CORS_ORIGINS + +# Check if infrastructure is running (shared or per-environment) +echo "๐Ÿ” Checking infrastructure..." +INFRA_RUNNING=true + +# Check for MongoDB by container name and status +if docker ps --filter "name=^chronicle-mongo$" --format '{{.Names}}' | grep -q chronicle-mongo; then + echo "โœ… MongoDB: chronicle-mongo" +else + echo "โš ๏ธ MongoDB not running" + INFRA_RUNNING=false +fi + +# Check for Redis by container name and status +if docker ps --filter "name=^chronicle-redis$" --format '{{.Names}}' | grep -q chronicle-redis; then + echo "โœ… Redis: chronicle-redis" +else + echo "โš ๏ธ Redis not running" + INFRA_RUNNING=false +fi + +# Check for Qdrant by container name and status +if docker ps --filter "name=^chronicle-qdrant$" --format '{{.Names}}' | grep -q chronicle-qdrant; then + echo "โœ… Qdrant: chronicle-qdrant" +else + echo "โš ๏ธ Qdrant not running" + INFRA_RUNNING=false +fi + +# Check for Neo4j if enabled in ANY environment +NEO4J_NEEDED=false +NEO4J_RUNNING=false +if grep -q "^NEO4J_ENABLED=true" environments/*.env 2>/dev/null; then + NEO4J_NEEDED=true + if docker ps --filter "name=^chronicle-neo4j$" --format '{{.Names}}' | grep -q chronicle-neo4j; then + echo "โœ… Neo4j: chronicle-neo4j" + NEO4J_RUNNING=true + else + echo "โš ๏ธ Neo4j not running (required by at least one environment)" + # Don't mark core infrastructure as not running - Neo4j can be started separately + fi +fi + +# Check for Caddy if configured +if [ "$USE_CADDY_PROXY" = "true" ]; then + if docker ps --filter "name=^chronicle-caddy$" --format '{{.Names}}' | grep -q chronicle-caddy; then + echo "โœ… Caddy: chronicle-caddy" + else + echo "โ„น๏ธ Caddy not running (will be started)" + # Don't mark infrastructure as not running - Caddy is optional + fi +fi + +# Auto-start infrastructure if not running +if [ "$INFRA_RUNNING" = "false" ]; then + echo "" + echo "๐Ÿš€ Starting shared infrastructure..." + echo "" + + # Create network if it doesn't exist + docker network inspect chronicle-network >/dev/null 2>&1 || docker network create chronicle-network + + # Clean up any orphaned network endpoints for this environment + docker network inspect chronicle-network --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | \ + grep "${COMPOSE_PROJECT_NAME}" | \ + xargs -n1 docker network disconnect -f chronicle-network 2>/dev/null || true + + # Check if infrastructure containers exist but are stopped (exited or created status) + STOPPED_CONTAINERS=$(docker ps -a --filter "status=exited" --filter "status=created" --format "{{.Names}}" 2>/dev/null | grep -E '^chronicle-(mongo|redis|qdrant|neo4j|caddy)$' || true) + + if [ -n "$STOPPED_CONTAINERS" ]; then + echo " Found stopped infrastructure containers: $STOPPED_CONTAINERS" + echo " Starting stopped containers..." + echo "$STOPPED_CONTAINERS" | xargs docker start 2>/dev/null || true + + # If Neo4j is needed but not in stopped containers, start it with compose + if [ "$NEO4J_NEEDED" = "true" ] && ! echo "$STOPPED_CONTAINERS" | grep -q "neo4j"; then + echo " Starting Neo4j (required by at least one environment)..." + # Clean up any orphaned network endpoint first + docker network disconnect -f chronicle-network chronicle-neo4j 2>/dev/null || true + docker compose -p chronicle-infra -f compose/infrastructure-shared.yml --profile neo4j up -d --remove-orphans neo4j + fi + else + # Start infrastructure with compose + # Add Neo4j profile if enabled in ANY environment + if [ "$NEO4J_NEEDED" = "true" ]; then + echo " Starting infrastructure with Neo4j support (required by at least one environment)..." + docker compose -p chronicle-infra -f compose/infrastructure-shared.yml --profile neo4j up -d --remove-orphans + else + docker compose -p chronicle-infra -f compose/infrastructure-shared.yml up -d --remove-orphans + fi + fi + + # Wait for services to be ready + echo "" + echo "โณ Waiting for infrastructure to be ready..." + sleep 5 + + echo "โœ… Infrastructure started" + echo "" +fi + +# Start Neo4j separately if needed but not running +if [ "$NEO4J_NEEDED" = "true" ] && [ "$NEO4J_RUNNING" = "false" ]; then + echo "๐Ÿ”— Starting Neo4j (required by at least one environment)..." + echo "" + + # Remove any existing Neo4j container in Created/Exited state + docker rm -f chronicle-neo4j 2>/dev/null || true + + # Clean up any orphaned network endpoint + docker network disconnect -f chronicle-network chronicle-neo4j 2>/dev/null || true + + # Start Neo4j with compose under infrastructure project + # Use -p to explicitly set project to chronicle-infra + docker compose -p chronicle-infra -f compose/infrastructure-shared.yml --profile neo4j up -d neo4j + + echo "" + echo "โœ… Neo4j started" + echo "" +fi # Display configuration echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" @@ -104,17 +316,38 @@ echo "" echo "๐Ÿ“ฆ Project: ${COMPOSE_PROJECT_NAME}" echo "๐Ÿ—„๏ธ MongoDB Database: ${MONGODB_DATABASE}" echo "๐Ÿ—„๏ธ Mycelia Database: ${MYCELIA_DB}" +if [ "$NEO4J_ENABLED" = "true" ]; then + echo "๐Ÿ”— Neo4j Database: ${OPENMEMORY_DB} (graph memory enabled)" +fi echo "๐Ÿ’พ Data Directory: ${DATA_DIR}" echo "" echo "๐ŸŒ Service URLs:" echo " Backend: http://localhost:${BACKEND_PORT}" echo " Web UI: http://localhost:${WEBUI_PORT}" -echo " MongoDB: mongodb://localhost:${MONGO_PORT}" -echo " Redis: redis://localhost:${REDIS_PORT}" -echo " Qdrant HTTP: http://localhost:${QDRANT_HTTP_PORT}" -echo " Qdrant gRPC: http://localhost:${QDRANT_GRPC_PORT}" +echo " MongoDB: mongodb://localhost:${MONGO_PORT} (shared)" +echo " Redis: redis://localhost:${REDIS_PORT} (shared)" +echo " Qdrant HTTP: http://localhost:${QDRANT_HTTP_PORT} (shared)" +echo " Qdrant gRPC: http://localhost:${QDRANT_GRPC_PORT} (shared)" +# Show Neo4j if it's running (means at least one environment uses it) +if docker ps --format '{{.Names}}' | grep -q '^chronicle-neo4j$'; then + echo " Neo4j Browser: http://localhost:${NEO4J_HTTP_PORT} (shared)" + echo " Neo4j Bolt: neo4j://localhost:${NEO4J_BOLT_PORT} (shared)" +fi echo "" +# Show Tailscale configuration if enabled +if [ -n "$TAILSCALE_HOSTNAME" ]; then + echo "๐ŸŒ Tailscale Configuration:" + echo " Hostname: ${TAILSCALE_HOSTNAME}" + echo " Backend URL: ${VITE_BACKEND_URL}" + echo " Remote Web UI: http://${TAILSCALE_HOSTNAME}:${WEBUI_PORT}" + echo " CORS: โœ… Tailscale URLs included" + echo "" + echo "๐Ÿ’ก Frontend is configured to use Tailscale backend" + echo " Access from any device: http://${TAILSCALE_HOSTNAME}:${WEBUI_PORT}" + echo "" +fi + # Show optional service URLs if enabled via --profile or SERVICES variable if [[ "$SERVICES" == *"mycelia"* ]] || [[ "$*" == *"mycelia"* ]]; then echo "๐Ÿ“Š Mycelia Services:" @@ -130,12 +363,14 @@ if [[ "$SERVICES" == *"speaker"* ]] || [[ "$*" == *"speaker"* ]]; then fi if [[ "$SERVICES" == *"openmemory"* ]] || [[ "$*" == *"openmemory"* ]]; then - echo "๐Ÿง  OpenMemory MCP:" - echo " Service: http://localhost:${OPENMEMORY_PORT}" + echo "๐Ÿง  OpenMemory:" + echo " API: http://localhost:${OPENMEMORY_PORT}" + echo " UI: http://localhost:${OPENMEMORY_UI_PORT}" + echo " User: ${OPENMEMORY_USER_ID:-user}" echo "" fi -if [[ "$SERVICES" == *"asr"* ]] || [[ "$*" == *"parakeet"* ]]; then +if [[ "$SERVICES" == *"asr"* ]] || [[ "$*" == *"asr"* ]]; then echo "๐Ÿ—ฃ๏ธ Parakeet ASR:" echo " Service: http://localhost:${PARAKEET_PORT}" echo "" @@ -143,8 +378,8 @@ fi echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" echo "" -# Create data directory -mkdir -p "${DATA_DIR}" +# Note: DATA_DIR is set for reference but actual data is stored in backends/advanced/data/ +# Data isolation is achieved via database names (MONGODB_DATABASE, MYCELIA_DB), not file directories # Update backends/advanced/.env with environment-specific values # This ensures the backend uses the correct database names @@ -163,7 +398,9 @@ fi echo "# It combines: docker-defaults.env + config-docker.env + .env.secrets + environments/${ENV_NAME}.env" echo "" echo "# Database configuration (environment-specific)" + echo "# NOTE: MONGODB_URI includes database name and should not be overridden" echo "MONGODB_URI=mongodb://mongo:27017/${MONGODB_DATABASE}" + echo "MONGODB_DATABASE=${MONGODB_DATABASE}" echo "MYCELIA_DB=${MYCELIA_DB}" echo "" echo "# All loaded environment variables" @@ -230,16 +467,25 @@ fi "MYCELIA_FRONTEND_PORT" "SPEAKER_PORT" "OPENMEMORY_PORT" + "OPENMEMORY_DB" "PARAKEET_PORT" + "NEO4J_HTTP_PORT" + "NEO4J_BOLT_PORT" "QDRANT_DATA_PATH" "REDIS_DATA_PATH" + "VITE_BACKEND_URL" + "CORS_ORIGINS" ) # Remove duplicates and sort config_vars=($(printf '%s\n' "${config_vars[@]}" | sort -u)) # Export only the allowlisted variables + # Skip MONGODB_URI and MONGODB_DATABASE as they're set explicitly above for key in "${config_vars[@]}"; do + if [ "$key" = "MONGODB_URI" ] || [ "$key" = "MONGODB_DATABASE" ]; then + continue + fi if [ -n "${!key}" ]; then echo "${key}=${!key}" fi @@ -257,4 +503,315 @@ echo "" echo "๐Ÿš€ Starting Docker Compose..." echo "" -docker compose "$@" up +# Build profile flags from SERVICES variable +PROFILE_FLAGS=() +if [ -n "$SERVICES" ]; then + for service in $SERVICES; do + PROFILE_FLAGS+=(--profile "$service") + done +fi + +# Generate Caddyfile if Caddy proxy is enabled +# Note: Caddy runs as a shared service, not per-environment +if [ "$USE_CADDY_PROXY" = "true" ]; then + echo "๐Ÿ”ง Generating Caddyfile for path-based routing..." + ./scripts/generate-caddyfile.sh + echo "" + + # Check if Tailscale certificates exist, provision if needed + if [ -n "$TAILSCALE_HOSTNAME" ]; then + # Create certs directory if it doesn't exist + mkdir -p certs + + CERT_FILE="certs/${TAILSCALE_HOSTNAME}.crt" + KEY_FILE="certs/${TAILSCALE_HOSTNAME}.key" + + if [ ! -f "$CERT_FILE" ]; then + echo "๐Ÿ” Tailscale certificates not found - provisioning now..." + echo "" + echo " Running: tailscale cert ${TAILSCALE_HOSTNAME}" + echo "" + + # Run tailscale cert and move files to certs directory + if tailscale cert "${TAILSCALE_HOSTNAME}" 2>&1; then + # Move certificates to certs directory + mv "${TAILSCALE_HOSTNAME}.crt" "certs/" + mv "${TAILSCALE_HOSTNAME}.key" "certs/" + + echo "" + echo "โœ… Certificates provisioned successfully!" + echo " Location: certs/${TAILSCALE_HOSTNAME}.{crt,key}" + echo "" + else + echo "" + echo "โŒ Certificate provisioning failed!" + echo "" + echo " This may happen if:" + echo " โ€ข Tailscale is not running (run: sudo tailscale up)" + echo " โ€ข You don't have permission to provision certs" + echo "" + echo " You can provision certificates manually with:" + echo " tailscale cert ${TAILSCALE_HOSTNAME}" + echo " mv ${TAILSCALE_HOSTNAME}.* certs/" + echo "" + exit 1 + fi + else + echo "โœ… Tailscale certificates found at: $CERT_FILE" + + # Check certificate expiry + if command -v openssl &> /dev/null; then + EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$CERT_FILE" 2>/dev/null | cut -d= -f2) + if [ -n "$EXPIRY_DATE" ]; then + EXPIRY_EPOCH=$(date -j -f "%b %d %T %Y %Z" "$EXPIRY_DATE" "+%s" 2>/dev/null || date -d "$EXPIRY_DATE" "+%s" 2>/dev/null) + CURRENT_EPOCH=$(date "+%s") + DAYS_UNTIL_EXPIRY=$(( ($EXPIRY_EPOCH - $CURRENT_EPOCH) / 86400 )) + + if [ $DAYS_UNTIL_EXPIRY -lt 0 ]; then + echo " โš ๏ธ Certificate EXPIRED ${DAYS_UNTIL_EXPIRY#-} days ago!" + echo " Run: tailscale cert ${TAILSCALE_HOSTNAME}" + echo "" + elif [ $DAYS_UNTIL_EXPIRY -lt 30 ]; then + echo " โš ๏ธ Certificate expires in ${DAYS_UNTIL_EXPIRY} days (${EXPIRY_DATE})" + echo " Consider renewing: tailscale cert ${TAILSCALE_HOSTNAME}" + echo "" + else + echo " Valid until: ${EXPIRY_DATE} (${DAYS_UNTIL_EXPIRY} days)" + echo "" + fi + fi + fi + fi + fi + + # Check if Caddy is running, start if needed + if docker ps --format '{{.Names}}' | grep -q '^chronicle-caddy$'; then + # Check if it's part of the infrastructure project + CADDY_PROJECT=$(docker inspect chronicle-caddy --format '{{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null) + if [ "$CADDY_PROJECT" = "chronicle-infra" ]; then + echo "โœ… Caddy is already running (chronicle-infra)" + echo " Access all environments at: https://${TAILSCALE_HOSTNAME}/" + echo "" + else + echo "โš ๏ธ Caddy is running but not in infrastructure project (project: $CADDY_PROJECT)" + echo "" + echo "๐Ÿ”„ Recreating Caddy in infrastructure project..." + echo "" + + # Stop and remove the old container + docker stop chronicle-caddy >/dev/null 2>&1 + docker rm chronicle-caddy >/dev/null 2>&1 + + # Start fresh from infrastructure-shared.yml + docker compose -f compose/infrastructure-shared.yml up -d caddy + + echo "" + echo "โœ… Caddy recreated in chronicle-infra project" + echo " Access all environments at: https://${TAILSCALE_HOSTNAME}/" + echo "" + fi + else + echo "โš ๏ธ Caddy is not running" + echo "" + echo "๐Ÿš€ Starting Caddy (shared infrastructure)..." + echo "" + + # Remove any stopped caddy containers from wrong projects + if docker ps -a --filter "name=chronicle-caddy" --format "{{.Names}}" 2>/dev/null | grep -q chronicle-caddy; then + echo " Removing old Caddy container..." + docker rm -f chronicle-caddy >/dev/null 2>&1 + fi + + # Start Caddy from infrastructure-shared.yml + echo " Starting: docker compose -f compose/infrastructure-shared.yml up -d caddy" + docker compose -f compose/infrastructure-shared.yml up -d caddy + + echo "" + echo "โœ… Caddy started in chronicle-infra project" + echo " Access all environments at: https://${TAILSCALE_HOSTNAME}/" + echo "" + fi +fi + +# Check if frontend needs rebuild for Tailscale or Caddy path-based routing +if [ -n "$TAILSCALE_HOSTNAME" ] && [ "$USE_CADDY_PROXY" != "true" ]; then + echo "โš ๏ธ Tailscale configured - forcing frontend rebuild with backend URL: ${VITE_BACKEND_URL}" + echo " (This ensures frontend can reach backend from remote devices)" + echo "" + # Force rebuild webui to pick up new VITE_BACKEND_URL + docker compose "${PROFILE_FLAGS[@]}" build --no-cache webui +elif [ "$USE_CADDY_PROXY" = "true" ]; then + if [ -n "$VITE_BASE_PATH" ] && [ "$VITE_BASE_PATH" != "/" ]; then + echo "๐Ÿ”„ Caddy path-based routing enabled - rebuilding WebUI with base path: ${VITE_BASE_PATH}" + echo " (This ensures routing works correctly at ${VITE_BASE_PATH})" + echo "" + docker compose "${PROFILE_FLAGS[@]}" build webui + else + echo "๐Ÿ”„ Caddy proxy enabled - frontend will use relative URLs (no rebuild needed)" + echo "" + fi +fi + +# Check if OpenMemory UI needs rebuild (bakes OPENMEMORY_USER_ID at build time) +if [[ "$SERVICES" == *"openmemory"* ]] || [[ "$*" == *"openmemory"* ]]; then + echo "๐Ÿง  OpenMemory enabled - rebuilding UI with user: ${OPENMEMORY_USER_ID:-user}" + echo " (Build args need to be baked into Next.js)" + echo "" + docker compose "${PROFILE_FLAGS[@]}" build openmemory-ui +fi + +# Clean up network endpoints only if force recreating +if [ "$FORCE_RECREATE" = "true" ]; then + echo "๐Ÿงน Cleaning up network endpoints (force mode)..." + docker network inspect chronicle-network --format '{{range .Containers}}{{.Name}}{{"\n"}}{{end}}' 2>/dev/null | \ + grep "^${COMPOSE_PROJECT_NAME}-" | \ + while read container; do + echo " Disconnecting: $container" + docker network disconnect -f chronicle-network "$container" 2>/dev/null || true + done +fi + +# Build docker compose command +COMPOSE_ARGS=("${PROFILE_FLAGS[@]}" "$@" "up" "-d") + +# Add --force-recreate if requested +if [ "$FORCE_RECREATE" = "true" ]; then + echo "๐Ÿ”„ Force recreating containers..." + COMPOSE_ARGS+=("--force-recreate") +fi + +# Start services +docker compose "${COMPOSE_ARGS[@]}" + +# Wait for services to be healthy +echo "" +echo "โณ Waiting for services to become healthy..." +sleep 5 + +# Clear screen and display service URLs +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "โœ… Services Started Successfully!" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "๐ŸŒ Access Your Services:" +echo "" +echo " ๐Ÿ“ฑ Web Dashboard: http://localhost:${WEBUI_PORT}" +echo " ๐Ÿ”Œ Backend API: http://localhost:${BACKEND_PORT}" +echo " ๐Ÿ“Š API Health: http://localhost:${BACKEND_PORT}/health" +echo " ๐Ÿ“š API Docs: http://localhost:${BACKEND_PORT}/docs" +echo "" + +# Show Caddy/Tailscale URLs if configured +if [ "$USE_CADDY_PROXY" = "true" ] && [ -n "$TAILSCALE_HOSTNAME" ]; then + echo "๐ŸŒ Caddy Reverse Proxy Access (Path-Based Routing):" + echo "" + echo " ๐Ÿ“ฑ ${ENV_NAME} Environment: https://${TAILSCALE_HOSTNAME}/${ENV_NAME}/" + echo " ๐Ÿ“Š Environment List: https://${TAILSCALE_HOSTNAME}/" + echo "" + echo " โ„น๏ธ Other environments:" + echo " https://${TAILSCALE_HOSTNAME}/dev/" + echo " https://${TAILSCALE_HOSTNAME}/test/" + echo " https://${TAILSCALE_HOSTNAME}/prod/" + echo "" + echo " ๐Ÿ”— API Endpoints:" + echo " Backend API: https://${TAILSCALE_HOSTNAME}/${ENV_NAME}/api/" + echo " WebSocket: wss://${TAILSCALE_HOSTNAME}/${ENV_NAME}/ws_pcm" + echo " API Docs: https://${TAILSCALE_HOSTNAME}/${ENV_NAME}/docs" + echo "" +elif command -v tailscale >/dev/null 2>&1 && tailscale status >/dev/null 2>&1; then + TAILSCALE_HOSTNAME=$(tailscale status --json 2>/dev/null | grep -A 20 '"Self"' | grep '"DNSName"' | cut -d'"' -f4 | sed 's/\.$//') + if [ -n "$TAILSCALE_HOSTNAME" ]; then + echo "๐ŸŒ Tailscale Access (from any device in your tailnet):" + echo "" + echo " ๐Ÿ“ฑ Web Dashboard: http://${TAILSCALE_HOSTNAME}:${WEBUI_PORT}" + echo " ๐Ÿ”Œ Backend API: http://${TAILSCALE_HOSTNAME}:${BACKEND_PORT}" + echo " ๐Ÿ“š API Docs: http://${TAILSCALE_HOSTNAME}:${BACKEND_PORT}/docs" + echo "" + fi +fi +echo "๐Ÿ—„๏ธ Database Connections:" +echo "" +echo " MongoDB: mongodb://localhost:${MONGO_PORT}" +echo " Redis: redis://localhost:${REDIS_PORT}" +echo " Qdrant HTTP: http://localhost:${QDRANT_HTTP_PORT}" +echo " Qdrant gRPC: localhost:${QDRANT_GRPC_PORT}" +# Show Neo4j if it's running (means at least one environment uses it) +if docker ps --format '{{.Names}}' | grep -q '^chronicle-neo4j$'; then + echo " Neo4j Browser: http://localhost:${NEO4J_HTTP_PORT}" + echo " Neo4j Bolt: neo4j://localhost:${NEO4J_BOLT_PORT}" +fi +echo "" + +# Show optional service URLs if they're running +if [[ "$SERVICES" == *"mycelia"* ]] || [[ "$*" == *"mycelia"* ]]; then + echo "๐Ÿ“Š Mycelia Memory Services:" + echo "" + echo " Backend API: http://localhost:${MYCELIA_BACKEND_PORT}" + echo " Web Interface: http://localhost:${MYCELIA_FRONTEND_PORT}" + echo "" +fi + +if [[ "$SERVICES" == *"speaker"* ]] || [[ "$*" == *"speaker"* ]]; then + echo "๐ŸŽค Speaker Recognition:" + echo "" + echo " Service API: http://localhost:${SPEAKER_PORT}" + echo "" +fi + +if [[ "$SERVICES" == *"openmemory"* ]] || [[ "$*" == *"openmemory"* ]]; then + echo "๐Ÿง  OpenMemory MCP:" + echo "" + echo " Service API: http://localhost:${OPENMEMORY_PORT}" + echo "" +fi + +if [[ "$SERVICES" == *"asr"* ]] || [[ "$*" == *"asr"* ]]; then + echo "๐Ÿ—ฃ๏ธ Parakeet ASR:" + echo "" + echo " Service API: http://localhost:${PARAKEET_PORT}" + echo "" +fi + +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "๐Ÿ“ Useful Commands:" +echo "" +echo " View logs: docker compose -p ${COMPOSE_PROJECT_NAME} logs -f" +echo " Stop services: docker compose -p ${COMPOSE_PROJECT_NAME} down" +echo " Restart: docker compose -p ${COMPOSE_PROJECT_NAME} restart" +echo " Status: docker compose -p ${COMPOSE_PROJECT_NAME} ps" +echo "" +echo "๐Ÿ’พ Environment: ${ENV_NAME}" +echo "๐Ÿ“ฆ Project Name: ${COMPOSE_PROJECT_NAME}" +echo "๐Ÿ“‚ Data Directory: ${DATA_DIR}" +echo "๐Ÿ—„๏ธ Database: ${MONGODB_DATABASE}" +if [ "$NEO4J_ENABLED" = "true" ]; then + echo "๐Ÿ”— Neo4j Database: ${OPENMEMORY_DB}" +fi +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" + +# Determine and display main URL +MAIN_URL="" +if [ "$USE_CADDY_PROXY" = "true" ] && [ -n "$TAILSCALE_HOSTNAME" ]; then + # Caddy with path-based routing + MAIN_URL="https://${TAILSCALE_HOSTNAME}/${ENV_NAME}/" +elif command -v tailscale >/dev/null 2>&1 && tailscale status >/dev/null 2>&1; then + # Check if tailscale serve is configured for root path + TAILSCALE_HOSTNAME_DETECT=$(tailscale status --json 2>/dev/null | grep -A 20 '"Self"' | grep '"DNSName"' | cut -d'"' -f4 | sed 's/\.$//') + if [ -n "$TAILSCALE_HOSTNAME_DETECT" ] && tailscale serve status 2>/dev/null | grep -q "^|-- /"; then + # Tailscale serve is configured + MAIN_URL="https://${TAILSCALE_HOSTNAME_DETECT}/" + else + # Tailscale available but serve not configured, use port-based + MAIN_URL="http://localhost:${WEBUI_PORT}" + fi +else + # Local access only + MAIN_URL="http://localhost:${WEBUI_PORT}" +fi + +echo "๐ŸŒ Main URL: ${MAIN_URL}" +echo "" From 1bf70d135a60ce6f0101ef040d048032074d55e8 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Tue, 9 Dec 2025 22:15:30 +0000 Subject: [PATCH 15/21] added docs for how mycelia works remopved openmemory to make submodule --- .gitmodules | 4 + .../Docs/mycelia-auth-and-ownership.md | 263 ++++++++++ backends/advanced/Docs/mycelia-auto-login.md | 199 +++++++ backends/advanced/Docs/mycelia-setup.md | 363 +++++++++++++ .../advanced/compose/optional-services.yml | 98 ---- extras/openmemory | 1 + extras/openmemory-mcp/.env.template | 11 - extras/openmemory-mcp/.gitignore | 1 - extras/openmemory-mcp/README.md | 187 ------- extras/openmemory-mcp/docker-compose.yml | 67 --- extras/openmemory-mcp/init-cache.sh | 31 -- extras/openmemory-mcp/requirements.txt | 1 - extras/openmemory-mcp/run.sh | 77 --- extras/openmemory-mcp/setup.sh | 99 ---- extras/openmemory-mcp/test_standalone.py | 492 ------------------ 15 files changed, 830 insertions(+), 1064 deletions(-) create mode 100644 backends/advanced/Docs/mycelia-auth-and-ownership.md create mode 100644 backends/advanced/Docs/mycelia-auto-login.md create mode 100644 backends/advanced/Docs/mycelia-setup.md delete mode 100644 backends/advanced/compose/optional-services.yml create mode 160000 extras/openmemory delete mode 100644 extras/openmemory-mcp/.env.template delete mode 100644 extras/openmemory-mcp/.gitignore delete mode 100644 extras/openmemory-mcp/README.md delete mode 100644 extras/openmemory-mcp/docker-compose.yml delete mode 100755 extras/openmemory-mcp/init-cache.sh delete mode 100644 extras/openmemory-mcp/requirements.txt delete mode 100755 extras/openmemory-mcp/run.sh delete mode 100755 extras/openmemory-mcp/setup.sh delete mode 100755 extras/openmemory-mcp/test_standalone.py diff --git a/.gitmodules b/.gitmodules index ffffaa52..c89cd889 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "extras/mycelia"] path = extras/mycelia url = https://github.com/mycelia-tech/mycelia +[submodule "extras/openmemory"] + path = extras/openmemory + url = https://github.com/thestumonkey/mem0.git + branch = friend-lite-integration diff --git a/backends/advanced/Docs/mycelia-auth-and-ownership.md b/backends/advanced/Docs/mycelia-auth-and-ownership.md new file mode 100644 index 00000000..c1fc24cd --- /dev/null +++ b/backends/advanced/Docs/mycelia-auth-and-ownership.md @@ -0,0 +1,263 @@ +# Mycelia Authentication & Object Ownership + +## How Mycelia Determines the Logged-In User + +### 1. Token Extraction +```typescript +// From extras/mycelia/backend/app/lib/auth/core.server.ts:82-133 + +async authenticate(req: Request): Promise { + // Extract token from: + // 1. Authorization header: "Bearer " + // 2. Query parameter: ?token= + // 3. URL search params + + // Try Friend-Lite JWT first + let auth = await verifyFriendLiteToken(token); + + if (!auth) { + // Fall back to Mycelia native token + auth = await verifyToken(token); + } + + return auth; +} +``` + +### 2. JWT Verification & Auth Object Creation + +#### Friend-Lite JWT +```typescript +// extras/mycelia/backend/app/lib/auth/friend-lite-jwt.ts:36-86 + +export const verifyFriendLiteToken = async (token: string): Promise => { + const { payload } = await jwtVerify(token, secret); + + // Extract principal from JWT (sub for Friend-Lite, owner/principal for OAuth) + const principal = payload.sub || payload.principal || payload.owner; + + // Create Auth object + const auth = new Auth({ + principal, // THIS is the user ID used for everything! + policies: [{ resource: "**", action: "*", effect: "allow" }], + }); + + return auth; +} +``` + +#### OAuth Token (Client Credentials) +```typescript +// extras/mycelia/backend/app/lib/auth/tokens.ts:138-154 + +async decodeAccessToken(apiKey: string): Promise { + const keyDoc = await verifyApiKey(apiKey); + + return signJWT( + keyDoc.owner, // owner field (Friend-Lite user ID) + keyDoc._id!.toString(), // principal field (API key ID) โš ๏ธ + keyDoc.policies, + duration, + ); +} + +// The resulting JWT has: +// { +// "owner": "692c7727c7b16bdf58d23cd1", // Friend-Lite user +// "principal": "692d76235ef8d25e060ad9f6", // API key ID +// "policies": [...] +// } +``` + +### 3. Auth Object Structure + +```typescript +class Auth { + principal: string; // THIS is what everything is scoped by! + policies: Policy[]; + + constructor(options: { policies?: Policy[]; principal: string }) { + this.policies = options.policies || []; + this.principal = options.principal; // โญ KEY FIELD + } +} +``` + +## How Object Ownership Works + +### 1. Object Creation (Auto-inject userId) +```typescript +// extras/mycelia/backend/app/lib/objects/resource.server.ts:261-286 + +case "create": { + const doc = { + ...input.object, + userId: auth.principal, // โญ Auto-inject from JWT principal + version: 1, + createdAt: new Date(), + updatedAt: new Date(), + }; + + await mongo({ action: "insertOne", collection: "objects", doc }); +} +``` + +### 2. Object Retrieval (Auto-scope by userId) +```typescript +// extras/mycelia/backend/app/lib/objects/resource.server.ts:289-303 + +case "get": { + const object = await mongo({ + action: "findOne", + collection: "objects", + query: { + _id: objectId, + userId: auth.principal // โญ Auto-scope by user + }, + }); + if (!object) throw new Error("Object not found"); + return object; +} +``` + +### 3. Object Listing (Auto-scope by userId) +```typescript +// extras/mycelia/backend/app/lib/objects/resource.server.ts:412-441 + +case "list": { + let query = input.filters || {}; + + // Auto-scope all queries by user + query = { + ...query, + userId: auth.principal, // โญ ALWAYS filtered by principal + }; + + const results = await mongo({ + action: "find", + collection: "objects", + query, + options: { limit, skip, sort } + }); + return results; +} +``` + +### 4. Object Updates & Deletes (Auto-scope by userId) +```typescript +case "update": +case "delete": { + // First, find the object - ensures it belongs to this user + const current = await mongo({ + action: "findOne", + collection: "objects", + query: { _id: objectId, userId: auth.principal }, // โญ Verify ownership + }); + if (!current) throw new Error("Object not found"); + + // Then perform the update/delete + await mongo({ ... }); +} +``` + +## The Authentication Flow Summary + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1. HTTP Request with JWT Token โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 2. Mycelia `authenticate()` function โ”‚ +โ”‚ โ€ข Extracts token from Authorization header โ”‚ +โ”‚ โ€ข Tries Friend-Lite JWT verification first โ”‚ +โ”‚ โ€ข Falls back to Mycelia native JWT โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 3. JWT Verification โ”‚ +โ”‚ Friend-Lite JWT: โ”‚ +โ”‚ โ†’ principal = payload.sub (user ID) โ”‚ +โ”‚ OAuth Token: โ”‚ +โ”‚ โ†’ principal = keyDoc._id.toString() (API key ID) โ”‚ +โ”‚ โ”‚ +โ”‚ Creates: Auth { principal, policies } โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 4. Object Operations โ”‚ +โ”‚ CREATE: doc.userId = auth.principal โ”‚ +โ”‚ GET: query { _id, userId: auth.principal } โ”‚ +โ”‚ LIST: query { ..., userId: auth.principal } โ”‚ +โ”‚ UPDATE: query { _id, userId: auth.principal } โ”‚ +โ”‚ DELETE: query { _id, userId: auth.principal } โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## The Problem: OAuth Token Principal Mismatch + +### Friend-Lite Memory Creation (via JWT) +```python +# Friend-Lite generates JWT with actual user ID +jwt_token = generate_jwt_for_user(user_id, user_email) +# JWT payload: { "sub": "692c7727c7b16bdf58d23cd1", "email": "..." } + +# Mycelia extracts principal from sub field +# principal = "692c7727c7b16bdf58d23cd1" + +# Object created with: +# { "userId": "692c7727c7b16bdf58d23cd1", ... } +``` + +### Mycelia OAuth Token Access +```javascript +// OAuth client credentials exchange +// API key has: { owner: "692c7727c7b16bdf58d23cd1", _id: "692d76235ef8d25e060ad9f6" } + +// Mycelia generates JWT with API key ID as principal +// JWT payload: { "principal": "692d76235ef8d25e060ad9f6", ... } + +// Object queries filtered by: +// { "userId": "692d76235ef8d25e060ad9f6" } + +// โŒ Mismatch! Objects have userId="692c7727..." but query expects "692d7623..." +``` + +## The Answer + +**Q: If I create a new object via Friend-Lite, will I be able to access it via Mycelia OAuth?** + +**A: No**, because: + +1. Friend-Lite creates objects with `userId = "692c7727c7b16bdf58d23cd1"` (actual user ID) +2. Mycelia OAuth token has `principal = "692d76235ef8d25e060ad9f6"` (API key ID) +3. All Mycelia queries filter by `userId == principal` +4. **Mismatch** โ†’ Objects not visible via OAuth! + +## Solutions + +### Option 1: Use Friend-Lite Auto-Login (Recommended โœ…) +Access Mycelia through Friend-Lite at **http://localhost:5173/memories** +- Uses Friend-Lite JWT directly +- Principal matches object userId +- All objects accessible! + +### Option 2: Fix Mycelia's OAuth Implementation (Requires Code Change) +Modify `extras/mycelia/backend/app/lib/auth/tokens.ts`: +```typescript +export async function decodeAccessToken(...) { + const keyDoc = await verifyApiKey(apiKey); + + return signJWT( + keyDoc.owner, + keyDoc.owner, // โฌ…๏ธ Use owner as principal, not _id! + keyDoc.policies, + duration, + ); +} +``` + +This would make OAuth tokens use the actual user ID as principal, matching object ownership. diff --git a/backends/advanced/Docs/mycelia-auto-login.md b/backends/advanced/Docs/mycelia-auto-login.md new file mode 100644 index 00000000..4349815b --- /dev/null +++ b/backends/advanced/Docs/mycelia-auto-login.md @@ -0,0 +1,199 @@ +# Mycelia Auto-Login and OAuth Access + +Friend-Lite supports **two methods** for accessing Mycelia, giving you flexibility for different use cases. + +## Method 1: Auto-Login (Web Frontend) โœ… RECOMMENDED + +**Use Case**: Seamless access when using both Friend-Lite and Mycelia web frontends in the same browser. + +**How it works**: +1. User logs into Friend-Lite web dashboard (`/login`) +2. Friend-Lite stores JWT in `localStorage['mycelia_jwt_token']` +3. User opens Mycelia frontend (same browser) +4. Mycelia automatically detects the JWT and logs in +5. **No manual configuration needed!** + +**Implementation**: +- Friend-Lite: `webui/src/contexts/AuthContext.tsx` - Stores JWT on login +- Mycelia: `extras/mycelia/frontend/src/lib/auth.ts:40-44` - Reads JWT from localStorage + +**User Experience**: +``` +User Login Flow: +1. Visit http://localhost:5173 (Friend-Lite) +2. Login with email/password +3. Visit http://localhost:3002 (Mycelia) +4. Already logged in! โœจ +``` + +## Method 2: OAuth Client Credentials (API Access) + +**Use Case**: +- Direct API access to Mycelia +- CLI tools and scripts +- Standalone Mycelia usage (without Friend-Lite frontend) +- Third-party integrations + +**How it works**: +1. On startup, Friend-Lite creates OAuth credentials for admin user (if `MEMORY_PROVIDER=mycelia`) +2. Credentials are logged to console and stored in Friend-Lite database +3. Use Client ID + Client Secret to authenticate with Mycelia API + +**Implementation**: +- `services/mycelia_sync.py` - Sync service that creates OAuth credentials +- `app_factory.py:76-82` - Calls sync on startup +- Mycelia: `extras/mycelia/backend/app/lib/auth/tokens.ts` - Verifies OAuth credentials + +**Startup Logs**: +``` +๐Ÿ”‘ MYCELIA OAUTH CREDENTIALS (Save these!) +====================================================================== +User: admin@example.com +Client ID: 67a4f2e1b3c9d8e5f6a7b8c9 +Client Secret: mycelia_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456 +====================================================================== +Configure Mycelia frontend at http://localhost:3002/settings +====================================================================== +``` + +**API Usage Example**: +```bash +# Exchange credentials for access token +curl -X POST http://localhost:5100/oauth/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=YOUR_CLIENT_ID" \ + -d "client_secret=YOUR_CLIENT_SECRET" + +# Use access token for API calls +curl -X POST http://localhost:5100/api/resource/tech.mycelia.objects \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"action": "list", "filters": {}, "options": {"limit": 10}}' +``` + +**Manual Frontend Configuration**: +1. Visit http://localhost:3002/settings +2. Enter the Client ID and Client Secret from startup logs +3. Save settings +4. Mycelia will use OAuth instead of auto-login + +## Authentication Priority + +Mycelia frontend checks authentication in this order: + +1. **localStorage JWT** (`mycelia_jwt_token`) - Auto-login from Friend-Lite +2. **OAuth credentials** (from Settings page) - Manual configuration +3. **No authentication** - Shows login/settings UI + +## User Isolation and Object Ownership + +Both authentication methods use the **same user ID** as the principal: + +- **Friend-Lite JWT**: `principal = payload.sub` (Friend-Lite user ID) +- **OAuth Token**: `principal = api_key.owner` (Friend-Lite user ID) + +This ensures: +โœ… Objects created via Friend-Lite are accessible via Mycelia +โœ… Objects created via Mycelia are accessible via Friend-Lite +โœ… All objects have `userId = friend_lite_user_id` +โœ… Data isolation works correctly across both systems + +## Configuration + +### Environment Variables + +```bash +# Friend-Lite +AUTH_SECRET_KEY=your-super-secret-jwt-key-here +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=your-secure-password + +# Mycelia (must match Friend-Lite) +JWT_SECRET=your-super-secret-jwt-key-here # Same as AUTH_SECRET_KEY! + +# Memory Provider (enables OAuth sync) +MEMORY_PROVIDER=mycelia # OAuth credentials created on startup +``` + +### MongoDB Collections + +**Friend-Lite Database** (`friend-lite`): +- `users` collection: + ```json + { + "_id": ObjectId("692c7727c7b16bdf58d23cd1"), + "email": "admin@example.com", + "mycelia_oauth": { + "client_id": "67a4f2e1b3c9d8e5f6a7b8c9", + "created_at": "2025-12-01T10:30:00Z", + "synced": true + } + } + ``` + +**Mycelia Database** (`mycelia` or `mycelia_test`): +- `api_keys` collection: + ```json + { + "_id": ObjectId("67a4f2e1b3c9d8e5f6a7b8c9"), + "owner": "692c7727c7b16bdf58d23cd1", // Friend-Lite user ID + "hashedKey": "...", + "salt": "...", + "name": "Friend-Lite Auto (admin@example.com)", + "policies": [{"resource": "**", "action": "*", "effect": "allow"}], + "isActive": true + } + ``` + +## Debugging + +### Check Auto-Login Status + +Open browser console on Mycelia frontend: +```javascript +// Check if Friend-Lite JWT exists +localStorage.getItem('mycelia_jwt_token') + +// Check OAuth settings +JSON.parse(localStorage.getItem('mycelia-settings')) +``` + +### Verify OAuth Credentials + +```bash +# Check Friend-Lite database +mongosh mongodb://localhost:27017/friend-lite +db.users.findOne({email: "admin@example.com"}, {mycelia_oauth: 1}) + +# Check Mycelia database +mongosh mongodb://localhost:27017/mycelia +db.api_keys.find({owner: "692c7727c7b16bdf58d23cd1"}) +``` + +### Common Issues + +**Issue**: "Can't see objects in Mycelia" +- **Check**: JWT principal matches object userId +- **Solution**: Use auto-login (localStorage) for seamless experience + +**Issue**: "OAuth token invalid" +- **Check**: JWT_SECRET in Mycelia matches AUTH_SECRET_KEY in Friend-Lite +- **Solution**: Ensure environment variables are identical + +**Issue**: "No OAuth credentials in logs" +- **Check**: `MEMORY_PROVIDER=mycelia` is set +- **Check**: Admin user exists in Friend-Lite database +- **Solution**: Check startup logs for errors + +## Summary + +| Feature | Auto-Login | OAuth | +|---------|-----------|-------| +| **Use Case** | Web frontend | API/CLI access | +| **Setup** | Automatic | Manual or auto-sync | +| **User Experience** | Seamless | Requires credentials | +| **Principal** | `sub` from JWT | `owner` from API key | +| **Best For** | End users | Developers/integrations | + +**Recommendation**: Use auto-login for normal web usage, OAuth for API access and integrations. diff --git a/backends/advanced/Docs/mycelia-setup.md b/backends/advanced/Docs/mycelia-setup.md new file mode 100644 index 00000000..f7a26d8a --- /dev/null +++ b/backends/advanced/Docs/mycelia-setup.md @@ -0,0 +1,363 @@ +# Mycelia Setup Guide + +Mycelia is an advanced memory management interface that provides a rich, interactive way to explore and manage your Friend-Lite memories. This guide explains how to set up and access Mycelia with Friend-Lite. + +## What is Mycelia? + +Mycelia is a separate frontend application that connects to Friend-Lite's memory system. It provides: +- Advanced memory visualization and exploration +- Graph-based memory relationships +- Rich query and filtering capabilities +- API access for programmatic memory management + +## Prerequisites + +Before setting up Mycelia, ensure Friend-Lite is installed and running: + +```bash +cd backends/advanced +docker compose up --build -d +``` + +Verify Friend-Lite is accessible at http://localhost:5173 + +## Setup Methods + +There are **two ways** to access Mycelia, depending on your use case: + +### Method 1: Auto-Login (Recommended for Web UI) + +**Best for**: Regular users accessing Mycelia through the web interface + +**How it works**: When you log into Friend-Lite, it automatically stores your authentication token. When you open Mycelia in the same browser, it detects this token and logs you in automatically. + +**Setup Steps**: + +1. **Enable Mycelia in Friend-Lite** + + Edit your `.env` file: + ```bash + MEMORY_PROVIDER=mycelia # Enable Mycelia integration + ``` + +2. **Start Mycelia services** + + ```bash + docker compose --profile mycelia up --build -d + ``` + +3. **Login to Friend-Lite** + + - Visit http://localhost:5173 + - Login with your credentials (e.g., admin@example.com) + +4. **Open Mycelia** + + - Visit http://localhost:3002 + - **You're automatically logged in!** โœจ + +**No manual configuration needed** - the authentication happens automatically via browser localStorage. + +--- + +### Method 2: OAuth Client Credentials (For API Access) + +**Best for**: +- API/CLI access to Mycelia +- Programmatic memory management +- Third-party integrations +- Standalone Mycelia usage + +**How it works**: Friend-Lite generates OAuth credentials (Client ID and Client Secret) that can be used to authenticate with Mycelia's API. + +**Setup Steps**: + +1. **Enable Mycelia and start services** + + Edit your `.env` file: + ```bash + MEMORY_PROVIDER=mycelia + ``` + + Start services: + ```bash + docker compose --profile mycelia up --build -d + ``` + +2. **Find your OAuth credentials** + + When Friend-Lite starts with `MEMORY_PROVIDER=mycelia`, it automatically creates OAuth credentials. Check the startup logs: + + ```bash + docker compose logs friend-backend | grep -A 10 "MYCELIA OAUTH" + ``` + + You'll see output like: + ``` + ๐Ÿ”‘ MYCELIA OAUTH CREDENTIALS (Save these!) + ====================================================================== + User: admin@example.com + Client ID: 67a4f2e1b3c9d8e5f6a7b8c9 + Client Secret: mycelia_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456 + ====================================================================== + Configure Mycelia frontend at http://localhost:3002/settings + ====================================================================== + ``` + + **โš ๏ธ IMPORTANT: Save these credentials securely!** They provide full API access to your memories. + +3. **Configure Mycelia Frontend (Optional)** + + If you want to use OAuth instead of auto-login in the web UI: + + - Visit http://localhost:3002/settings + - Enter your **Client ID** and **Client Secret** + - Click Save + + Mycelia will now use OAuth authentication instead of auto-login. + +4. **Use OAuth for API Access** + + **Get an access token**: + ```bash + curl -X POST http://localhost:5100/oauth/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=YOUR_CLIENT_ID" \ + -d "client_secret=YOUR_CLIENT_SECRET" + ``` + + Response: + ```json + { + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer" + } + ``` + + **Use the token to access Mycelia API**: + ```bash + curl -X POST http://localhost:5100/api/resource/tech.mycelia.objects \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "action": "list", + "filters": {}, + "options": {"limit": 10} + }' + ``` + +## Service URLs + +When running with Mycelia enabled: + +| Service | URL | Description | +|---------|-----|-------------| +| Friend-Lite Web UI | http://localhost:5173 | Main Friend-Lite dashboard | +| Friend-Lite API | http://localhost:8000 | Backend API | +| Mycelia Frontend | http://localhost:3002 | Mycelia memory interface | +| Mycelia API | http://localhost:5100 | Mycelia backend API | + +## Configuration + +### Required Environment Variables + +```bash +# Friend-Lite Authentication (Required) +AUTH_SECRET_KEY=your-super-secret-jwt-key-here +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=your-secure-admin-password + +# Memory Provider (Required for Mycelia) +MEMORY_PROVIDER=mycelia + +# LLM Configuration (Required for memory extraction) +LLM_PROVIDER=openai +OPENAI_API_KEY=your-openai-key-here +OPENAI_MODEL=gpt-4o-mini + +# Database (Shared between Friend-Lite and Mycelia) +MONGODB_URI=mongodb://mongo:27017 +``` + +### Mycelia-Specific Configuration + +The following variables are automatically set by docker-compose when using the mycelia profile: + +```bash +# Mycelia Backend +MYCELIA_PORT=5100 +MYCELIA_DB=mycelia # Separate database for Mycelia objects + +# JWT Secret (Must match Friend-Lite!) +JWT_SECRET=your-super-secret-jwt-key-here # Same as AUTH_SECRET_KEY +``` + +## Verification + +### Check Auto-Login + +Open browser console on http://localhost:3002: + +```javascript +// Check if Friend-Lite JWT exists +localStorage.getItem('mycelia_jwt_token') +// Should show: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +### Check OAuth Credentials in Database + +```bash +# Connect to MongoDB +docker compose exec mongo mongosh + +# Check Friend-Lite user +use friend-lite +db.users.findOne({email: "admin@example.com"}, {mycelia_oauth: 1}) + +# Check Mycelia API key +use mycelia +db.api_keys.find({name: {$regex: "Friend-Lite Auto"}}) +``` + +### Test Memory Sync + +1. Create a memory in Friend-Lite (upload audio or use chat) +2. Open Mycelia at http://localhost:3002 +3. Your memories should appear automatically + +## Troubleshooting + +### Issue: Can't see Mycelia OAuth credentials + +**Cause**: `MEMORY_PROVIDER` not set to `mycelia` + +**Solution**: +```bash +# Check current setting +docker compose exec friend-backend env | grep MEMORY_PROVIDER + +# If not set, edit .env and restart +echo "MEMORY_PROVIDER=mycelia" >> .env +docker compose restart friend-backend +docker compose logs friend-backend | grep -A 10 "MYCELIA OAUTH" +``` + +### Issue: Auto-login doesn't work + +**Cause**: JWT not stored in localStorage + +**Solution**: +1. Ensure you logged into Friend-Lite first at http://localhost:5173 +2. Check browser console on http://localhost:3002: + ```javascript + localStorage.getItem('mycelia_jwt_token') + ``` +3. If null, re-login to Friend-Lite + +### Issue: Can't see objects in Mycelia + +**Cause**: JWT principal doesn't match object userId + +**Solution**: +- Use auto-login (recommended) - ensures principal matches +- Verify JWT secret matches in both services: + ```bash + docker compose exec friend-backend env | grep AUTH_SECRET_KEY + docker compose exec mycelia-backend env | grep JWT_SECRET + ``` + +### Issue: OAuth token exchange fails + +**Cause**: Wrong credentials or JWT secret mismatch + +**Solution**: +1. Verify credentials from logs: + ```bash + docker compose logs friend-backend | grep -A 10 "MYCELIA OAUTH" + ``` +2. Check JWT secrets match (see above) +3. Verify API key in database: + ```bash + docker compose exec mongo mongosh + use mycelia + db.api_keys.find().pretty() + ``` + +### Issue: "Connection refused" to Mycelia + +**Cause**: Mycelia services not running + +**Solution**: +```bash +# Start with Mycelia profile +docker compose --profile mycelia up --build -d + +# Verify services are running +docker compose ps | grep mycelia +``` + +## Security Considerations + +### JWT Secret + +**CRITICAL**: `AUTH_SECRET_KEY` (Friend-Lite) and `JWT_SECRET` (Mycelia) **MUST match**! + +If they don't match: +- Auto-login won't work (JWT verification fails) +- Data isolation will be broken (wrong user IDs) + +### OAuth Credentials + +- **Client Secret** provides full API access - treat it like a password +- Never commit credentials to version control +- Rotate credentials if compromised (delete API key from Mycelia database) +- Use separate credentials for different applications/users + +### Network Security + +For production deployments: +- Use HTTPS for all services +- Restrict Mycelia API access (port 5100) to authorized networks +- Consider using API gateway or reverse proxy +- Implement rate limiting on OAuth endpoints + +## Advanced: Multiple Users + +Each Friend-Lite user automatically gets their own OAuth credentials: + +1. Users created via Friend-Lite get auto-synced to Mycelia +2. Each user has isolated memory access +3. OAuth credentials are per-user +4. Data isolation is enforced at database level + +To get credentials for a different user: +```bash +# Login as that user in Friend-Lite +# Check logs for their credentials +docker compose logs friend-backend | grep "User:.*$EMAIL" -A 5 +``` + +## Additional Resources + +- **[Mycelia Auth Details](mycelia-auth-and-ownership.md)** - Deep dive into authentication flow +- **[Mycelia Auto-Login](mycelia-auto-login.md)** - Auto-login implementation details +- **[Test Environment](mycelia-test-environment.md)** - Running Mycelia in test mode +- **[Memory Providers Guide](memory-configuration-guide.md)** - Compare Friend-Lite vs Mycelia memory systems + +## Summary + +### For Web UI Users (Recommended) +1. Set `MEMORY_PROVIDER=mycelia` in `.env` +2. Run `docker compose --profile mycelia up --build -d` +3. Login to Friend-Lite at http://localhost:5173 +4. Visit Mycelia at http://localhost:3002 (auto-login!) + +### For API Users +1. Set `MEMORY_PROVIDER=mycelia` in `.env` +2. Run `docker compose --profile mycelia up --build -d` +3. Get credentials from logs: `docker compose logs friend-backend | grep -A 10 "MYCELIA OAUTH"` +4. Exchange credentials for access token via OAuth +5. Use access token for API calls + +Both methods provide the same memory access - choose based on your use case! diff --git a/backends/advanced/compose/optional-services.yml b/backends/advanced/compose/optional-services.yml deleted file mode 100644 index b6d19131..00000000 --- a/backends/advanced/compose/optional-services.yml +++ /dev/null @@ -1,98 +0,0 @@ -# Optional Services -# Services that can be enabled via profiles or are commented out by default -# All services below are commented out by default - uncomment as needed - -services: {} - -# Commented out services below - uncomment as needed: - - # Ollama - Local LLM service - # Uncomment to use local LLM instead of OpenAI - # ollama: - # image: ollama/ollama:latest - # container_name: ollama - # ports: - # - "11434:11434" - # volumes: - # - ollama_data:/root/.ollama - # networks: - # - chronicle-network - # # Uncomment for GPU support: - # # deploy: - # # resources: - # # reservations: - # # devices: - # # - driver: nvidia - # # count: all - # # capabilities: [gpu] - - # Neo4j - Graph database for advanced memory relationships - # Uncomment if using neo4j-based memory provider - # neo4j-mem0: - # image: neo4j:5.15-community - # ports: - # - "7474:7474" # HTTP - # - "7687:7687" # Bolt - # environment: - # - NEO4J_AUTH=neo4j/${NEO4J_PASSWORD:-password} - # - NEO4J_PLUGINS=["apoc"] - # - NEO4J_dbms_security_procedures_unrestricted=apoc.* - # - NEO4J_dbms_security_procedures_allowlist=apoc.* - # volumes: - # - neo4j_data:/data - # - neo4j_logs:/logs - # restart: unless-stopped - # networks: - # - chronicle-network - - # OpenMemory MCP Server - # Uncomment if using openmemory_mcp memory provider - # openmemory-mcp: - # build: - # context: ../../extras/openmemory-mcp/cache/mem0/openmemory/api - # dockerfile: Dockerfile - # env_file: - # - .env - # environment: - # - OPENAI_API_KEY=${OPENAI_API_KEY} - # - OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory} - # depends_on: - # - qdrant - # ports: - # - "8765:8765" - # restart: unless-stopped - # healthcheck: - # test: ["CMD", "python", "-c", "import requests; exit(0 if requests.get('http://localhost:8765/docs').status_code == 200 else 1)"] - # interval: 30s - # timeout: 10s - # retries: 3 - # start_period: 30s - # networks: - # - chronicle-network - - # Ngrok - Expose to internet for testing - # UNCOMMENT FOR LOCAL DEMO - EXPOSES to internet - # Use Tailscale instead for production - # ngrok: - # image: ngrok/ngrok:latest - # depends_on: [friend-backend, proxy] - # ports: - # - "4040:4040" # Ngrok web interface - # environment: - # - NGROK_AUTHTOKEN=${NGROK_AUTHTOKEN} - # command: "http proxy:80 --url=${NGROK_URL} --basic-auth=${NGROK_BASIC_AUTH}" - # networks: - # - chronicle-network - -networks: - chronicle-network: - name: chronicle-network - external: true - -volumes: - ollama_data: - driver: local - neo4j_data: - driver: local - neo4j_logs: - driver: local diff --git a/extras/openmemory b/extras/openmemory new file mode 160000 index 00000000..cc3675f4 --- /dev/null +++ b/extras/openmemory @@ -0,0 +1 @@ +Subproject commit cc3675f465b7e16aac19408de8692fa5bfc2c2aa diff --git a/extras/openmemory-mcp/.env.template b/extras/openmemory-mcp/.env.template deleted file mode 100644 index 10c790bd..00000000 --- a/extras/openmemory-mcp/.env.template +++ /dev/null @@ -1,11 +0,0 @@ -# OpenMemory MCP Configuration -# Copy this file to .env and fill in your values - -# Required: OpenAI API Key for memory processing -OPENAI_API_KEY= - -# Optional: User identifier (defaults to system username) -USER=openmemory - -# Optional: Frontend URL (if using UI) -NEXT_PUBLIC_API_URL=http://localhost:8765 \ No newline at end of file diff --git a/extras/openmemory-mcp/.gitignore b/extras/openmemory-mcp/.gitignore deleted file mode 100644 index 6e25fa8f..00000000 --- a/extras/openmemory-mcp/.gitignore +++ /dev/null @@ -1 +0,0 @@ -cache/ \ No newline at end of file diff --git a/extras/openmemory-mcp/README.md b/extras/openmemory-mcp/README.md deleted file mode 100644 index 82d033e0..00000000 --- a/extras/openmemory-mcp/README.md +++ /dev/null @@ -1,187 +0,0 @@ -# OpenMemory MCP Service - -This directory contains a local deployment of the OpenMemory MCP (Model Context Protocol) server, which can be used as an alternative memory provider for Friend-Lite. - -## What is OpenMemory MCP? - -OpenMemory MCP is a memory service from mem0.ai that provides: -- Automatic memory extraction from conversations -- Vector-based memory storage with Qdrant -- Semantic search across memories -- MCP protocol support for AI integrations -- Built-in deduplication and memory management - -## Quick Start - -### 1. Configure Environment - -```bash -cp .env.template .env -# Edit .env and add your OPENAI_API_KEY -``` - -### 2. Start Services - -```bash -# Start backend only (recommended) -./run.sh - -# Or start with UI (optional) -./run.sh --with-ui -``` - -### 3. Configure Friend-Lite - -In your Friend-Lite backend `.env` file: - -```bash -# Use OpenMemory MCP instead of built-in memory processing -MEMORY_PROVIDER=openmemory_mcp -OPENMEMORY_MCP_URL=http://localhost:8765 -``` - -## Architecture - -The deployment includes: - -1. **OpenMemory MCP Server** (port 8765) - - FastAPI backend with MCP protocol support - - Memory extraction using OpenAI - - REST API and MCP endpoints - -2. **Qdrant Vector Database** (port 6334) - - Stores memory embeddings - - Enables semantic search - - Isolated from main Friend-Lite Qdrant - -3. **OpenMemory UI** (port 3001, optional) - - Web interface for memory management - - View and search memories - - Debug and testing interface - -## Service Endpoints - -- **MCP Server**: http://localhost:8765 - - REST API: `/api/v1/memories` - - MCP SSE: `/mcp/{client_name}/sse/{user_id}` - -- **Qdrant Dashboard**: http://localhost:6334/dashboard - -- **UI** (if enabled): http://localhost:3001 - -## How It Works with Friend-Lite - -When configured with `MEMORY_PROVIDER=openmemory_mcp`, Friend-Lite will: - -1. Send raw conversation transcripts to OpenMemory MCP -2. OpenMemory extracts memories using OpenAI -3. Memories are stored in the dedicated Qdrant instance -4. Friend-Lite can search memories via the MCP protocol - -This replaces Friend-Lite's built-in memory processing with OpenMemory's implementation. - -## Managing Services - -```bash -# View logs -docker compose logs -f - -# Stop services -docker compose down - -# Stop and remove data -docker compose down -v - -# Restart services -docker compose restart -``` - -## Testing - -### Standalone Test (No Friend-Lite Dependencies) - -Test the OpenMemory MCP server directly: - -```bash -# From extras/openmemory-mcp directory -./test_standalone.py - -# Or with custom server URL -OPENMEMORY_MCP_URL=http://localhost:8765 python test_standalone.py -``` - -This test verifies: -- Server connectivity -- Memory creation via REST API -- Memory listing and search -- Memory deletion -- MCP protocol endpoints - -### Integration Test (With Friend-Lite) - -Test the integration between Friend-Lite and OpenMemory MCP: - -```bash -# From backends/advanced directory -cd backends/advanced -uv run python tests/test_openmemory_integration.py - -# Or with custom server URL -OPENMEMORY_MCP_URL=http://localhost:8765 uv run python tests/test_openmemory_integration.py -``` - -This test verifies: -- MCP client functionality -- OpenMemoryMCPService implementation -- Service factory integration -- Memory operations through Friend-Lite interface - -## Troubleshooting - -### Port Conflicts - -If ports are already in use, edit `docker-compose.yml`: -- Change `8765:8765` to another port for MCP server -- Change `6334:6333` to another port for Qdrant -- Update Friend-Lite's `OPENMEMORY_MCP_URL` accordingly - -### Memory Not Working - -1. Check OpenMemory logs: `docker compose logs openmemory-mcp` -2. Verify OPENAI_API_KEY is set correctly -3. Ensure Friend-Lite backend is configured with correct URL -4. Test MCP endpoint: `curl http://localhost:8765/api/v1/memories?user_id=test` - -### Connection Issues - -- Ensure containers are on same network if running Friend-Lite in Docker -- Use `host.docker.internal` instead of `localhost` when connecting from Docker containers - -## Advanced Configuration - -### Using with Docker Network - -If Friend-Lite backend is also running in Docker: - -```yaml -# In Friend-Lite docker-compose.yml -networks: - default: - external: - name: openmemory-mcp_openmemory-network -``` - -Then use container names in Friend-Lite .env: -```bash -OPENMEMORY_MCP_URL=http://openmemory-mcp:8765 -``` - -### Custom Models - -OpenMemory uses OpenAI by default. To use different models, you would need to modify the OpenMemory source code and build a custom image. - -## Resources - -- [OpenMemory Documentation](https://docs.mem0.ai/open-memory/introduction) -- [MCP Protocol Spec](https://github.com/mem0ai/mem0/tree/main/openmemory) -- [Friend-Lite Memory Docs](../../backends/advanced/MEMORY_PROVIDERS.md) \ No newline at end of file diff --git a/extras/openmemory-mcp/docker-compose.yml b/extras/openmemory-mcp/docker-compose.yml deleted file mode 100644 index 383d96f0..00000000 --- a/extras/openmemory-mcp/docker-compose.yml +++ /dev/null @@ -1,67 +0,0 @@ -services: - # Qdrant vector database for OpenMemory (following original naming) - mem0_store: - image: qdrant/qdrant - ports: - - "6335:6333" # Different port to avoid conflict with main Qdrant - volumes: - - ./data/mem0_storage:/qdrant/storage - restart: unless-stopped - - # OpenMemory MCP Server (built from local cache) - openmemory-mcp: - build: - context: ./cache/mem0/openmemory/api - dockerfile: Dockerfile - env_file: - - .env.openmemory - environment: - - OPENAI_API_KEY=${OPENAI_API_KEY} - - OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-stu} - depends_on: - - mem0_store - ports: - - "8765:8765" - restart: unless-stopped - healthcheck: - test: ["CMD", "python", "-c", "import requests; exit(0 if requests.get('http://localhost:8765/docs').status_code == 200 else 1)"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - - # OpenMemory UI (optional - can be disabled if not needed) - openmemory-ui: - image: mem0/openmemory-ui:latest - ports: - - "3001:3000" # Different port to avoid conflict - environment: - - NEXT_PUBLIC_API_URL=http://localhost:8765 - - NEXT_PUBLIC_USER_ID=openmemory - depends_on: - - openmemory-mcp - profiles: - - ui # Only starts when --profile ui is used - - # neo4j-mem0: - # image: neo4j:5.15-community - # ports: - # - "7474:7474" # HTTP - # - "7687:7687" # Bolt - # environment: - # - NEO4J_AUTH=neo4j/${NEO4J_PASSWORD:-password} - # - NEO4J_PLUGINS=["apoc"] - # - NEO4J_dbms_security_procedures_unrestricted=apoc.* - # - NEO4J_dbms_security_procedures_allowlist=apoc.* - # volumes: - # - ./data/neo4j_data:/data - # - ./data/neo4j_logs:/logs - # restart: unless-stopped - - - - -networks: - default: - name: chronicle-network - external: true \ No newline at end of file diff --git a/extras/openmemory-mcp/init-cache.sh b/extras/openmemory-mcp/init-cache.sh deleted file mode 100755 index 18ec6f6f..00000000 --- a/extras/openmemory-mcp/init-cache.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# Initialize or update local cached mem0 from Ankush's fork - -CACHE_DIR="./cache/mem0" -FORK_REPO="https://github.com/AnkushMalaker/mem0.git" -BRANCH="fix/get-endpoint" - -echo "๐Ÿ”„ Updating OpenMemory cache from fork..." - -if [ ! -d "$CACHE_DIR/.git" ]; then - echo "๐Ÿ“ฅ Initializing cache from fork..." - rm -rf "$CACHE_DIR" - git clone "$FORK_REPO" "$CACHE_DIR" - cd "$CACHE_DIR" - git checkout "$BRANCH" - echo "โœ… Cache initialized from $FORK_REPO ($BRANCH)" -else - echo "๐Ÿ”„ Updating existing cache..." - cd "$CACHE_DIR" - git fetch origin - git checkout "$BRANCH" - git pull origin "$BRANCH" - echo "โœ… Cache updated from $FORK_REPO ($BRANCH)" -fi - -echo "" -echo "๐Ÿ“‚ Cache directory: $(pwd)" -echo "๐ŸŒฟ Current branch: $(git branch --show-current)" -echo "๐Ÿ“ Latest commit: $(git log --oneline -1)" -echo "" -echo "๐Ÿš€ Ready to build! Run: docker compose build openmemory-mcp --no-cache" \ No newline at end of file diff --git a/extras/openmemory-mcp/requirements.txt b/extras/openmemory-mcp/requirements.txt deleted file mode 100644 index 486db2a8..00000000 --- a/extras/openmemory-mcp/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -httpx>=0.24.0 \ No newline at end of file diff --git a/extras/openmemory-mcp/run.sh b/extras/openmemory-mcp/run.sh deleted file mode 100755 index 1cc0bf21..00000000 --- a/extras/openmemory-mcp/run.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -set -e - -echo "๐Ÿš€ Starting OpenMemory MCP installation for Friend-Lite..." - -# Set environment variables -OPENAI_API_KEY="${OPENAI_API_KEY:-}" -USER="${USER:-$(whoami)}" - -# Check for .env file first -if [ -f .env ]; then - echo "๐Ÿ“ Loading configuration from .env file..." - export $(cat .env | grep -v '^#' | xargs) -fi - -if [ -z "$OPENAI_API_KEY" ]; then - echo "โŒ OPENAI_API_KEY not set." - echo " Option 1: Create a .env file from .env.template and add your key" - echo " Option 2: Run with: OPENAI_API_KEY=your_api_key ./run.sh" - echo " Option 3: Export it: export OPENAI_API_KEY=your_api_key" - exit 1 -fi - -# Check if Docker is installed -if ! command -v docker &> /dev/null; then - echo "โŒ Docker not found. Please install Docker first." - exit 1 -fi - -# Check if docker compose is available -if ! docker compose version &> /dev/null; then - echo "โŒ Docker Compose not found. Please install Docker Compose V2." - exit 1 -fi - -# Export required variables for Compose -export OPENAI_API_KEY -export USER - -# Parse command line arguments -PROFILE="" -if [ "$1" = "--with-ui" ]; then - PROFILE="--profile ui" - echo "๐ŸŽจ UI will be enabled at http://localhost:3001" -fi - -# Start services -echo "๐Ÿš€ Starting OpenMemory MCP services..." -docker compose up -d $PROFILE - -# Wait for services to be ready -echo "โณ Waiting for services to be ready..." -sleep 5 - -# Check if services are running -if docker ps | grep -q openmemory-mcp; then - echo "โœ… OpenMemory MCP Backend: http://localhost:8765" - echo "โœ… OpenMemory Qdrant: http://localhost:6334" - if [ "$1" = "--with-ui" ]; then - echo "โœ… OpenMemory UI: http://localhost:3001" - echo "โœ… OpenMemory MCP API: http://localhost:8765/openapi.json" - echo " Available endpoints:" - curl -s http://localhost:8765/openapi.json | jq '.paths | keys[]' - fi - echo "" - echo "๐Ÿ“š Integration with Friend-Lite:" - echo " Set MEMORY_PROVIDER=openmemory_mcp in your Friend-Lite .env" - echo " Set OPENMEMORY_MCP_URL=http://localhost:8765 in your Friend-Lite .env" - echo "" - echo "๐Ÿ” Check logs: docker compose logs -f" - echo "๐Ÿ›‘ Stop services: docker compose down" -else - echo "โŒ Failed to start OpenMemory MCP services" - echo " Check logs: docker compose logs" - exit 1 -fi \ No newline at end of file diff --git a/extras/openmemory-mcp/setup.sh b/extras/openmemory-mcp/setup.sh deleted file mode 100755 index 555720ec..00000000 --- a/extras/openmemory-mcp/setup.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash - -# Enable strict error handling -set -euo pipefail - -# Parse command line arguments -OPENAI_API_KEY="" - -while [[ $# -gt 0 ]]; do - case $1 in - --openai-api-key) - OPENAI_API_KEY="$2" - shift 2 - ;; - *) - echo "Unknown argument: $1" - exit 1 - ;; - esac -done - -echo "๐Ÿง  OpenMemory MCP Setup" -echo "======================" - -# Check if already configured -if [ -f ".env" ]; then - echo "โš ๏ธ .env already exists. Backing up..." - cp .env .env.backup.$(date +%Y%m%d_%H%M%S) -fi - -# Start from template - check existence first -if [ ! -r ".env.template" ]; then - echo "Error: .env.template not found or not readable" >&2 - exit 1 -fi - -# Copy template and set secure permissions -if ! cp .env.template .env; then - echo "Error: Failed to copy .env.template to .env" >&2 - exit 1 -fi - -# Set restrictive permissions (owner read/write only) -chmod 600 .env - -# Clone the custom fork of mem0 with OpenMemory fixes -echo "" -echo "๐Ÿ“ฆ Setting up custom mem0 fork with OpenMemory..." -if [ -d "cache/mem0" ]; then - echo " Removing existing mem0 directory..." - rm -rf cache/mem0 -fi - -echo " Cloning mem0 fork from AnkushMalaker/mem0..." -mkdir -p cache -git clone https://github.com/AnkushMalaker/mem0.git cache/mem0 -cd cache/mem0 -echo " Checking out fix/get-endpoint branch..." -git checkout fix/get-endpoint -cd ../.. - -echo "โœ… Custom mem0 fork ready with OpenMemory improvements" - -# Get OpenAI API Key (prompt only if not provided via command line) -if [ -z "$OPENAI_API_KEY" ]; then - echo "" - echo "๐Ÿ”‘ OpenAI API Key (required for memory extraction)" - echo "Get yours from: https://platform.openai.com/api-keys" - while true; do - read -s -r -p "OpenAI API Key: " OPENAI_API_KEY - echo # Print newline after silent input - if [ -n "$OPENAI_API_KEY" ]; then - break - fi - echo "Error: OpenAI API Key cannot be empty. Please try again." - done -else - echo "โœ… OpenAI API key configured from command line" -fi - -# Update .env file safely using awk - replace existing line or append if missing -temp_file=$(mktemp) -awk -v key="$OPENAI_API_KEY" ' - /^OPENAI_API_KEY=/ { print "OPENAI_API_KEY=" key; found=1; next } - { print } - END { if (!found) print "OPENAI_API_KEY=" key } -' .env > "$temp_file" -mv "$temp_file" .env - -echo "" -echo "โœ… OpenMemory MCP configured!" -echo "๐Ÿ“ Configuration saved to .env" -echo "" -echo "๐Ÿš€ To start: docker compose up --build -d" -echo "๐ŸŒ MCP Server: http://localhost:8765" -echo "๐Ÿ“ฑ Web Interface: http://localhost:8765" -echo "๐Ÿ”ง UI (optional): docker compose --profile ui up -d" -echo "" -echo "๐Ÿ’ก Note: Using custom mem0 fork from AnkushMalaker/mem0:fix/get-endpoint" \ No newline at end of file diff --git a/extras/openmemory-mcp/test_standalone.py b/extras/openmemory-mcp/test_standalone.py deleted file mode 100755 index 58f011a4..00000000 --- a/extras/openmemory-mcp/test_standalone.py +++ /dev/null @@ -1,492 +0,0 @@ -#!/usr/bin/env python3 -"""Standalone test script for OpenMemory MCP server. - -This script tests the OpenMemory MCP server directly using its REST API, -without any dependencies on Friend-Lite backend code. -""" - -import asyncio -import json -import os -import subprocess -import sys -from typing import List, Dict, Any -import httpx -from pathlib import Path - -# Test Configuration Flags (following project patterns) -# TODO: Update CLAUDE.md documentation to reflect FRESH_RUN flag usage across all integration tests -# This replaces any previous "CACHED_MODE" references with consistent FRESH_RUN naming -FRESH_RUN = os.environ.get("FRESH_RUN", "true").lower() == "true" -CLEANUP_CONTAINERS = os.environ.get("CLEANUP_CONTAINERS", "false").lower() == "true" # Default false for dev convenience -REBUILD = os.environ.get("REBUILD", "false").lower() == "true" - - -class OpenMemoryClient: - """Simple client for testing OpenMemory REST API.""" - - def __init__(self, server_url: str = "http://localhost:8765", user_id: str = "test_user"): - self.server_url = server_url.rstrip('/') - self.user_id = user_id - self.client = httpx.AsyncClient(timeout=30.0) - - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - await self.client.aclose() - - async def test_connection(self) -> bool: - """Test if server is reachable.""" - try: - response = await self.client.get(f"{self.server_url}/") - return response.status_code in [200, 404, 422] - except: - return False - - async def create_memory(self, text: str) -> Dict[str, Any]: - """Create a new memory.""" - response = await self.client.post( - f"{self.server_url}/api/v1/memories/", - json={ - "user_id": self.user_id, - "text": text, - "metadata": { - "source": "test_script", - "test": True - }, - "infer": True, - "app": "openmemory" # Use default app name that exists - } - ) - response.raise_for_status() - return response.json() - - async def list_memories(self, limit: int = 10) -> List[Dict[str, Any]]: - """List memories for the user.""" - response = await self.client.get( - f"{self.server_url}/api/v1/memories/", - params={ - "user_id": self.user_id, - "page": 1, - "size": limit - } - ) - response.raise_for_status() - result = response.json() - - # Handle paginated response - if isinstance(result, dict) and "items" in result: - return result["items"] - elif isinstance(result, list): - return result - return [] - - async def search_memories(self, query: str, limit: int = 5) -> List[Dict[str, Any]]: - """Search memories with a query.""" - response = await self.client.get( - f"{self.server_url}/api/v1/memories/", - params={ - "user_id": self.user_id, - "search_query": query, - "page": 1, - "size": limit - } - ) - response.raise_for_status() - result = response.json() - - # Handle paginated response - if isinstance(result, dict) and "items" in result: - return result["items"] - elif isinstance(result, list): - return result - return [] - - async def delete_memories(self, memory_ids: List[str]) -> Dict[str, Any]: - """Delete specific memories.""" - response = await self.client.request( - "DELETE", - f"{self.server_url}/api/v1/memories/", - json={ - "memory_ids": memory_ids, - "user_id": self.user_id - } - ) - response.raise_for_status() - return response.json() - - async def get_stats(self) -> Dict[str, Any]: - """Get memory statistics.""" - try: - response = await self.client.get( - f"{self.server_url}/api/v1/stats/", - params={"user_id": self.user_id} - ) - response.raise_for_status() - return response.json() - except: - return {} - - -async def test_basic_operations(): - """Test basic OpenMemory operations.""" - - server_url = os.getenv("OPENMEMORY_MCP_URL", "http://localhost:8765") - # Use the same user ID as OpenMemory server expects - user_id = os.getenv("TEST_USER_ID", os.getenv("USER", "openmemory")) - - print(f"๐Ÿงช Testing OpenMemory MCP Server") - print(f"๐Ÿ“ Server URL: {server_url}") - print(f"๐Ÿ‘ค User ID: {user_id}") - print("="*60) - - async with OpenMemoryClient(server_url, user_id) as client: - # Test 1: Connection - print("\n1๏ธโƒฃ Testing connection...") - is_connected = await client.test_connection() - if not is_connected: - print("โŒ Failed to connect to OpenMemory server") - print(" Please ensure the server is running:") - print(" cd extras/openmemory-mcp && ./run.sh") - return False - print("โœ… Connected to OpenMemory server") - - # Test 2: Create memory - print("\n2๏ธโƒฃ Creating test memories...") - test_memories = [ - "I prefer Python for backend development and use FastAPI for building APIs.", - "My morning routine includes meditation at 6 AM followed by a 5-mile run.", - "I'm learning Japanese and practice with Anki flashcards for 30 minutes daily.", - "My favorite book is 'The Pragmatic Programmer' and I re-read it every year.", - "I work remotely from a co-working space in Seattle three days a week." - ] - - created_memories = [] - for i, text in enumerate(test_memories, 1): - try: - result = await client.create_memory(text) - if result is None: - # Handle None response (no-op, likely duplicate) - print(f" โ„น๏ธ Memory {i}: No-op (likely duplicate)") - elif isinstance(result, dict) and "error" in result: - print(f" โš ๏ธ Memory {i}: {result['error']}") - else: - # Handle successful creation or existing memory - if hasattr(result, 'id'): - memory_id = str(result.id) - else: - memory_id = result.get("id", f"memory_{i}") if isinstance(result, dict) else f"memory_{i}" - - created_memories.append(memory_id) - print(f" โœ… Memory {i}: Created (ID: {memory_id[:8]}...)") - except Exception as e: - print(f" โŒ Memory {i}: Failed - {e}") - - print(f"\n Summary: {len(created_memories)}/{len(test_memories)} memories created") - - # Test 3: List memories - print("\n3๏ธโƒฃ Listing memories...") - try: - memories = await client.list_memories(limit=20) - print(f"โœ… Found {len(memories)} memory(ies)") - - for i, memory in enumerate(memories[:3], 1): - content = memory.get("content", memory.get("text", ""))[:80] - memory_id = str(memory.get("id", "unknown"))[:8] - print(f" {i}. [{memory_id}...] {content}...") - except Exception as e: - print(f"โŒ Failed to list memories: {e}") - memories = [] - - # Test 4: Search memories - print("\n4๏ธโƒฃ Searching memories...") - test_queries = [ - "programming Python", - "morning exercise routine", - "learning languages" - ] - - for query in test_queries: - try: - results = await client.search_memories(query, limit=3) - print(f" Query: '{query}' โ†’ {len(results)} result(s)") - if results: - top_result = results[0] - content = top_result.get("content", top_result.get("text", ""))[:60] - print(f" Top: {content}...") - except Exception as e: - print(f" โŒ Search failed for '{query}': {e}") - - # Test 5: Get stats (if available) - print("\n5๏ธโƒฃ Getting statistics...") - try: - stats = await client.get_stats() - if stats: - print(f"โœ… Stats retrieved: {json.dumps(stats, indent=2)}") - else: - print("โ„น๏ธ No statistics available") - except Exception as e: - print(f"โ„น๏ธ Statistics endpoint not available: {e}") - - # Test 6: Delete memories (cleanup) - if memories and len(memories) > 0: - print("\n6๏ธโƒฃ Testing deletion...") - # Delete first memory as a test - test_memory_id = str(memories[0].get("id")) - try: - result = await client.delete_memories([test_memory_id]) - print(f"โœ… Deleted memory: {test_memory_id[:8]}...") - if "message" in result: - print(f" Response: {result['message']}") - except Exception as e: - print(f"โš ๏ธ Deletion not supported or failed: {e}") - - print("\n" + "="*60) - print("โœจ Test completed successfully!") - return True - - -async def test_mcp_protocol(): - """Test MCP protocol endpoints (if available).""" - - server_url = os.getenv("OPENMEMORY_MCP_URL", "http://localhost:8765") - user_id = os.getenv("TEST_USER_ID", os.getenv("USER", "openmemory")) - client_name = "test_client" - - print(f"\n๐Ÿ”ง Testing MCP Protocol Endpoints") - print(f"๐Ÿ“ Server URL: {server_url}") - print(f"๐Ÿ‘ค User ID: {user_id}") - print(f"๐Ÿท๏ธ Client: {client_name}") - print("="*60) - - async with httpx.AsyncClient(timeout=10.0) as client: # 10 second timeout - # Test MCP SSE endpoint - print("\n1๏ธโƒฃ Testing MCP SSE endpoint...") - try: - # SSE connections stay open, so we expect a timeout after connection opens - response = await client.get( - f"{server_url}/mcp/{client_name}/sse/{user_id}", - headers={"Accept": "text/event-stream"} - ) - # If we get here, connection opened successfully - print("โœ… MCP SSE endpoint is available") - except httpx.TimeoutException: - # This is expected - SSE connection opened but timed out waiting for events - print("โœ… MCP SSE endpoint is available (connection opened, timed out as expected)") - except Exception as e: - print(f"โ„น๏ธ MCP SSE endpoint not available: {e}") - - # Test MCP messages endpoint - print("\n2๏ธโƒฃ Testing MCP messages endpoint...") - try: - # Send a simple JSON-RPC request - payload = { - "jsonrpc": "2.0", - "id": "test_1", - "method": "initialize", - "params": {} - } - - response = await client.post( - f"{server_url}/mcp/messages/", # Add trailing slash to avoid redirect - json=payload, - headers={ - "Content-Type": "application/json", - "X-Client-Name": client_name, - "X-User-ID": user_id - } - ) - - if response.status_code == 200: - print("โœ… MCP messages endpoint is available") - result = response.json() - print(f" Response: {json.dumps(result, indent=2)[:200]}...") - else: - print(f"โ„น๏ธ MCP messages endpoint returned: {response.status_code}") - except Exception as e: - print(f"โ„น๏ธ MCP messages endpoint not available: {e}") - - print("\nโœจ MCP protocol test completed!") - - -def load_env_files(): - """Load environment from .env.test (priority) or .env (fallback), following project patterns.""" - try: - # Try to import python-dotenv for proper .env parsing - from dotenv import load_dotenv - - env_test_path = Path('.env.test') - env_path = Path('.env') - - if env_test_path.exists(): - print(f"๐Ÿ“„ Loading environment from {env_test_path}") - load_dotenv(env_test_path) - elif env_path.exists(): - print(f"๐Ÿ“„ Loading environment from {env_path}") - load_dotenv(env_path) - else: - print("โš ๏ธ No .env.test or .env file found, using shell environment") - except ImportError: - # Fallback to manual parsing if python-dotenv not available - print("โš ๏ธ python-dotenv not available, using simple parsing") - env_file = Path(".env") - if env_file.exists(): - print(f"๐Ÿ“„ Loading environment from {env_file}") - with open(env_file) as f: - for line in f: - line = line.strip() - if line and not line.startswith("#") and "=" in line: - key, value = line.split("=", 1) - os.environ[key] = value - else: - print("โš ๏ธ No .env file found, using shell environment") - - -def validate_required_keys(): - """Validate required API keys - FAIL FAST if missing.""" - missing_keys = [] - - if not os.getenv("OPENAI_API_KEY"): - missing_keys.append("OPENAI_API_KEY") - - if missing_keys: - print(f"โŒ FATAL ERROR: Missing required environment variables: {', '.join(missing_keys)}") - print(" These are required for OpenMemory to function.") - print(" Add to extras/openmemory-mcp/.env file:") - for key in missing_keys: - print(f" {key}=your-key-here") - print() - print(" Example:") - print(f" echo '{missing_keys[0]}=your-key-here' >> .env") - return False - - print(f"โœ… Required API keys validated") - return True - - -def cleanup_test_data(): - """Clean up OpenMemory test data if in fresh mode, following integration test patterns.""" - if not FRESH_RUN: - print("๐Ÿ—‚๏ธ Cache mode: Reusing existing memories and data") - return - - print("๐Ÿ—‚๏ธ Fresh mode: Cleaning existing memories and data...") - - # First, stop containers and remove volumes - try: - subprocess.run([ - "docker", "compose", "down", "-v" - ], check=True, cwd=Path.cwd()) - print(" โœ… Cleaned Docker volumes") - except subprocess.CalledProcessError as e: - print(f" โš ๏ธ Could not clean Docker volumes: {e}") - - # Then, clean data directories using lightweight Docker container (following project pattern) - try: - # Check if data directory exists - data_dir = Path.cwd() / "data" - if data_dir.exists(): - result = subprocess.run([ - "docker", "run", "--rm", - "-v", f"{data_dir}:/data", - "alpine:latest", - "sh", "-c", "rm -rf /data/*" - ], capture_output=True, text=True, timeout=30) - - if result.returncode == 0: - print(" โœ… Cleaned data directories") - else: - print(f" โš ๏ธ Error during data directory cleanup: {result.stderr}") - else: - print(" โ„น๏ธ No data directory to clean") - - except Exception as e: - print(f" โš ๏ธ Data directory cleanup failed: {e}") - print(" ๐Ÿ’ก Ensure Docker is running and accessible") - - -def cleanup_containers(): - """Stop and remove containers after test if cleanup enabled.""" - if not CLEANUP_CONTAINERS: - print("๐Ÿณ Keeping containers running for debugging") - return - - print("๐Ÿณ Cleaning up test containers...") - try: - subprocess.run([ - "docker", "compose", "down", "-v" - ], check=True, cwd=Path.cwd()) - print(" โœ… Containers cleaned up") - except subprocess.CalledProcessError as e: - print(f" โš ๏ธ Could not clean up containers: {e}") - - -async def main(): - """Run all standalone tests following integration test patterns.""" - - print("๐Ÿš€ OpenMemory MCP Standalone Tests") - print("="*60) - print(f"๐Ÿ”ง Configuration:") - print(f" FRESH_RUN={FRESH_RUN}, CLEANUP_CONTAINERS={CLEANUP_CONTAINERS}, REBUILD={REBUILD}") - print() - - # 1. Load environment files - load_env_files() - - # 2. Validate required keys - FAIL FAST - if not validate_required_keys(): - return False - - # 3. Data management - cleanup_test_data() - - # 4. Ensure containers are running (rebuild if requested) - if REBUILD: - print("๐Ÿ”จ Rebuilding containers...") - try: - subprocess.run([ - "docker", "compose", "build", "--no-cache" - ], check=True, cwd=Path.cwd()) - except subprocess.CalledProcessError as e: - print(f"โŒ Failed to rebuild containers: {e}") - return False - - # Start containers - print("๐Ÿณ Starting containers...") - try: - subprocess.run([ - "docker", "compose", "up", "-d" - ], check=True, cwd=Path.cwd()) - print(" โœ… Containers started") - except subprocess.CalledProcessError as e: - print(f"โŒ Failed to start containers: {e}") - return False - - # Wait a moment for services to be ready - print("โณ Waiting for services to be ready...") - await asyncio.sleep(5) - - try: - # 5. Run basic operations test - success = await test_basic_operations() - - if success: - # 6. Run MCP protocol test - await test_mcp_protocol() - - print(f"\n{'โœ…' if success else 'โŒ'} Test Results:") - print(f" Basic Operations: {'PASSED' if success else 'FAILED'}") - print(f" MCP Protocol: {'TESTED' if success else 'SKIPPED'}") - - return success - - finally: - # 7. Cleanup containers if requested - cleanup_containers() - - print("\n๐ŸŽ‰ All standalone tests completed!") - - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file From e6baecf85441144ba39e91453f3fb21f92fa3b9c Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Tue, 9 Dec 2025 22:16:40 +0000 Subject: [PATCH 16/21] use db name and use memory userid --- .../src/advanced_omi_backend/app_config.py | 3 +- .../src/advanced_omi_backend/database.py | 4 ++- .../src/advanced_omi_backend/models/job.py | 5 ++-- .../services/memory/providers/mcp_client.py | 30 ++++++++++++------- .../memory/providers/openmemory_mcp.py | 17 ++++++----- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/backends/advanced/src/advanced_omi_backend/app_config.py b/backends/advanced/src/advanced_omi_backend/app_config.py index 4caa70c5..4bef6593 100644 --- a/backends/advanced/src/advanced_omi_backend/app_config.py +++ b/backends/advanced/src/advanced_omi_backend/app_config.py @@ -28,8 +28,9 @@ class AppConfig: def __init__(self): # MongoDB Configuration self.mongodb_uri = os.getenv("MONGODB_URI", "mongodb://mongo:27017") + self.mongodb_database = os.getenv("MONGODB_DATABASE", "friend-lite") self.mongo_client = AsyncIOMotorClient(self.mongodb_uri) - self.db = self.mongo_client.get_default_database("friend-lite") + self.db = self.mongo_client.get_default_database(self.mongodb_database) self.users_col = self.db["users"] self.speakers_col = self.db["speakers"] diff --git a/backends/advanced/src/advanced_omi_backend/database.py b/backends/advanced/src/advanced_omi_backend/database.py index cca103ea..822878e5 100644 --- a/backends/advanced/src/advanced_omi_backend/database.py +++ b/backends/advanced/src/advanced_omi_backend/database.py @@ -14,6 +14,8 @@ # MongoDB Configuration MONGODB_URI = os.getenv("MONGODB_URI", "mongodb://mongo:27017") +MONGODB_DATABASE = os.getenv("MONGODB_DATABASE", "friend-lite") + mongo_client = AsyncIOMotorClient( MONGODB_URI, maxPoolSize=50, # Increased pool size for concurrent operations @@ -22,7 +24,7 @@ serverSelectionTimeoutMS=5000, # Fail fast if server unavailable socketTimeoutMS=20000, # 20 second timeout for operations ) -db = mongo_client.get_default_database("friend-lite") +db = mongo_client.get_default_database(MONGODB_DATABASE) # Collection references (for non-Beanie collections) users_col = db["users"] diff --git a/backends/advanced/src/advanced_omi_backend/models/job.py b/backends/advanced/src/advanced_omi_backend/models/job.py index 9d355ce5..b295782c 100644 --- a/backends/advanced/src/advanced_omi_backend/models/job.py +++ b/backends/advanced/src/advanced_omi_backend/models/job.py @@ -43,11 +43,12 @@ async def _ensure_beanie_initialized(): mongodb_uri = os.getenv("MONGODB_URI", "mongodb://localhost:27017") # Create MongoDB client + mongodb_database = os.getenv("MONGODB_DATABASE", "friend-lite") client = AsyncIOMotorClient(mongodb_uri) try: - database = client.get_default_database("friend-lite") + database = client.get_default_database(mongodb_database) except ConfigurationError: - database = client["friend-lite"] + database = client[mongodb_database] raise _beanie_initialized = True # Initialize Beanie diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py index 15226971..c0b9deaf 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/mcp_client.py @@ -111,20 +111,28 @@ async def add_memories(self, text: str) -> List[str]: # Use REST API endpoint for creating memories # The 'app' field can be either app name (string) or app UUID + payload = { + "user_id": self.user_id, + "text": text, + "app": self.client_name, # Use app name (OpenMemory accepts name or UUID) + "metadata": { + "source": "friend_lite", + "client": self.client_name, + "user_email": self.user_email + }, + "infer": True + } + + memory_logger.info(f"POSTing memory to {self.server_url}/api/v1/memories/ with payload={payload}") + response = await self.client.post( f"{self.server_url}/api/v1/memories/", - json={ - "user_id": self.user_id, - "text": text, - "app": self.client_name, # Use app name (OpenMemory accepts name or UUID) - "metadata": { - "source": "friend_lite", - "client": self.client_name, - "user_email": self.user_email - }, - "infer": True - } + json=payload ) + + response_body = response.text[:500] if response.status_code != 200 else "..." + memory_logger.info(f"OpenMemory response: status={response.status_code}, body={response_body}, headers={dict(response.headers)}") + response.raise_for_status() result = response.json() diff --git a/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py b/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py index d18be16a..970806a9 100644 --- a/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py +++ b/backends/advanced/src/advanced_omi_backend/services/memory/providers/openmemory_mcp.py @@ -142,18 +142,21 @@ async def add_memory( memory_logger.info(f"Skipping empty transcript for {source_id}") return True, [] - # Update MCP client user context for this operation + # Pass Friend-Lite user details to OpenMemory for proper user tracking + # OpenMemory will auto-create users if they don't exist original_user_id = self.mcp_client.user_id original_user_email = self.mcp_client.user_email - self.mcp_client.user_id = user_id # Use the actual Friend-Lite user's ID - self.mcp_client.user_email = user_email # Use the actual user's email + + # Update MCP client with Friend-Lite user details + self.mcp_client.user_id = user_id + self.mcp_client.user_email = user_email try: # Thin client approach: Send raw transcript to OpenMemory MCP server # OpenMemory handles: extraction, deduplication, vector storage, ACL enriched_transcript = f"[Source: {source_id}, Client: {client_id}] {transcript}" - memory_logger.info(f"Delegating memory processing to OpenMemory MCP for user {user_id}, source {source_id}") + memory_logger.info(f"Delegating memory processing to OpenMemory for user {user_id} (email: {user_email}), source {source_id}") memory_ids = await self.mcp_client.add_memories(text=enriched_transcript) finally: @@ -204,9 +207,9 @@ async def search_memories( if not self._initialized: await self.initialize() - # Update MCP client user context for this operation + # Update MCP client user context for this search operation original_user_id = self.mcp_client.user_id - self.mcp_client.user_id = user_id # Use the actual Friend-Lite user's ID + self.mcp_client.user_id = user_id try: results = await self.mcp_client.search_memory( @@ -231,7 +234,7 @@ async def search_memories( memory_logger.error(f"Search memories failed: {e}") return [] finally: - # Restore original user_id + # Restore original user context self.mcp_client.user_id = original_user_id async def get_all_memories( From 128774d8002b2eba8b473998e71ec1a4967b6759 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Tue, 9 Dec 2025 22:17:07 +0000 Subject: [PATCH 17/21] use instance localstorage to get auth token --- backends/advanced/webui/src/App.tsx | 7 ++- .../webui/src/contexts/AuthContext.tsx | 15 +++--- .../webui/src/hooks/useAudioRecording.ts | 54 ++++++------------- .../src/hooks/useSimpleAudioRecording.ts | 44 ++++++--------- .../webui/src/pages/Conversations.tsx | 5 +- backends/advanced/webui/src/services/api.ts | 48 ++++++++++++----- backends/advanced/webui/src/utils/storage.ts | 11 ++++ backends/advanced/webui/vite.config.ts | 3 +- 8 files changed, 96 insertions(+), 91 deletions(-) create mode 100644 backends/advanced/webui/src/utils/storage.ts diff --git a/backends/advanced/webui/src/App.tsx b/backends/advanced/webui/src/App.tsx index 6f7f3e72..fca59623 100644 --- a/backends/advanced/webui/src/App.tsx +++ b/backends/advanced/webui/src/App.tsx @@ -18,12 +18,15 @@ import { ErrorBoundary, PageErrorBoundary } from './components/ErrorBoundary' function App() { console.log('๐Ÿš€ Full App restored with working login!') - + + // Get base path from Vite config (e.g., "/prod/" for path-based routing) + const basename = import.meta.env.BASE_URL + return ( - + } /> (undefined) export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) - const [token, setToken] = useState(localStorage.getItem('token')) + const [token, setToken] = useState(localStorage.getItem(getStorageKey('token'))) const [isLoading, setIsLoading] = useState(true) // Check if user is admin @@ -30,7 +31,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { useEffect(() => { const initAuth = async () => { console.log('๐Ÿ” AuthContext: Initializing authentication...') - const savedToken = localStorage.getItem('token') + const savedToken = localStorage.getItem(getStorageKey('token')) console.log('๐Ÿ” AuthContext: Saved token exists:', !!savedToken) if (savedToken) { @@ -44,7 +45,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { } catch (error) { console.error('โŒ AuthContext: Token verification failed:', error) // Token is invalid, clear it - localStorage.removeItem('token') + localStorage.removeItem(getStorageKey('token')) setToken(null) setUser(null) } @@ -64,9 +65,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { const { access_token } = response.data setToken(access_token) - localStorage.setItem('token', access_token) + localStorage.setItem(getStorageKey('token'), access_token) // Store JWT for Mycelia auto-login (enables seamless access to Mycelia frontend) - localStorage.setItem('mycelia_jwt_token', access_token) + localStorage.setItem(getStorageKey('mycelia_jwt_token'), access_token) // Get user info const userResponse = await authApi.getMe() @@ -100,8 +101,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { const logout = () => { setUser(null) setToken(null) - localStorage.removeItem('token') - localStorage.removeItem('mycelia_jwt_token') + localStorage.removeItem(getStorageKey('token')) + localStorage.removeItem(getStorageKey('mycelia_jwt_token')) } return ( diff --git a/backends/advanced/webui/src/hooks/useAudioRecording.ts b/backends/advanced/webui/src/hooks/useAudioRecording.ts index 3e303cbc..dbb29889 100644 --- a/backends/advanced/webui/src/hooks/useAudioRecording.ts +++ b/backends/advanced/webui/src/hooks/useAudioRecording.ts @@ -1,4 +1,6 @@ import { useState, useRef, useCallback, useEffect } from 'react' +import { BACKEND_URL } from '../services/api' +import { getStorageKey } from '../utils/storage' export interface ComponentErrors { websocket: string | null @@ -126,48 +128,26 @@ export const useAudioRecording = (): UseAudioRecordingReturn => { setError(null) try { - const token = localStorage.getItem('token') + const token = localStorage.getItem(getStorageKey('token')) if (!token) { throw new Error('No authentication token found') } - // Build WebSocket URL using same logic as API service - let wsUrl: string - const { protocol, port } = window.location - // Check if we have a backend URL from environment - if (import.meta.env.VITE_BACKEND_URL) { - const backendUrl = import.meta.env.VITE_BACKEND_URL - const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:' - // Fallback logic based on current location - const isStandardPort = (protocol === 'https:' && (port === '' || port === '443')) || - (protocol === 'http:' && (port === '' || port === '80')) - - if (isStandardPort || backendUrl === '') { - // Use same origin for Ingress access - wsUrl = `${wsProtocol}//${window.location.host}/ws_pcm?token=${token}&device_name=webui-recorder` - } else if (backendUrl != undefined && backendUrl != '') { - wsUrl = `${wsProtocol}//${backendUrl}/ws_pcm?token=${token}&device_name=webui-recorder` - } - else if (port === '5173') { - // Development mode - wsUrl = `ws://localhost:8000/ws_pcm?token=${token}&device_name=webui-recorder` - } else { - // Fallback - use same origin instead of hardcoded port 8000 - wsUrl = `${wsProtocol}//${window.location.host}/ws_pcm?token=${token}&device_name=webui-recorder` - } + // Build WebSocket URL using BACKEND_URL from API service (handles base path correctly) + const { protocol } = window.location + const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:' + + let wsUrl: string + if (BACKEND_URL && BACKEND_URL.startsWith('http')) { + // BACKEND_URL is a full URL (e.g., http://localhost:8000) + const backendHost = BACKEND_URL.replace(/^https?:\/\//, '') + wsUrl = `${wsProtocol}//${backendHost}/ws_pcm?token=${token}&device_name=webui-recorder` + } else if (BACKEND_URL && BACKEND_URL !== '') { + // BACKEND_URL is a path (e.g., /prod) + wsUrl = `${wsProtocol}//${window.location.host}${BACKEND_URL}/ws_pcm?token=${token}&device_name=webui-recorder` } else { - // No environment variable set, use fallback logic - const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:' - const isStandardPort = (protocol === 'https:' && (port === '' || port === '443')) || - (protocol === 'http:' && (port === '' || port === '80')) - - if (isStandardPort) { - wsUrl = `${wsProtocol}//${window.location.host}/ws_pcm?token=${token}&device_name=webui-recorder` - } else if (port === '5173') { - wsUrl = `ws://localhost:8000/ws_pcm?token=${token}&device_name=webui-recorder` - } else { - wsUrl = `${wsProtocol}//${window.location.host}/ws_pcm?token=${token}&device_name=webui-recorder` - } + // BACKEND_URL is empty (same origin) + wsUrl = `${wsProtocol}//${window.location.host}/ws_pcm?token=${token}&device_name=webui-recorder` } const ws = new WebSocket(wsUrl) // Note: Don't set binaryType yet - will cause protocol violations with text messages diff --git a/backends/advanced/webui/src/hooks/useSimpleAudioRecording.ts b/backends/advanced/webui/src/hooks/useSimpleAudioRecording.ts index e0a1badc..cb3e3eee 100644 --- a/backends/advanced/webui/src/hooks/useSimpleAudioRecording.ts +++ b/backends/advanced/webui/src/hooks/useSimpleAudioRecording.ts @@ -1,4 +1,6 @@ import { useState, useRef, useCallback, useEffect } from 'react' +import { BACKEND_URL } from '../services/api' +import { getStorageKey } from '../utils/storage' export type RecordingStep = 'idle' | 'mic' | 'websocket' | 'audio-start' | 'streaming' | 'stopping' | 'error' export type RecordingMode = 'batch' | 'streaming' @@ -152,40 +154,26 @@ export const useSimpleAudioRecording = (): SimpleAudioRecordingReturn => { // Step 2: Connect WebSocket const connectWebSocket = useCallback(async (): Promise => { console.log('๐Ÿ”— Step 2: Connecting to WebSocket') - - const token = localStorage.getItem('token') + + const token = localStorage.getItem(getStorageKey('token')) if (!token) { throw new Error('No authentication token found') } - // Build WebSocket URL using same logic as API service + // Build WebSocket URL using BACKEND_URL from API service (handles base path correctly) + const { protocol } = window.location + const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:' + let wsUrl: string - const { protocol, port } = window.location - - // Check if we have a backend URL from environment - if (import.meta.env.VITE_BACKEND_URL) { - const backendUrl = import.meta.env.VITE_BACKEND_URL - const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:' - // Fallback logic based on current location - const isStandardPort = (protocol === 'https:' && (port === '' || port === '443')) || - (protocol === 'http:' && (port === '' || port === '80')) - - if (isStandardPort || backendUrl === '') { - // Use same origin for Ingress access - wsUrl = `${wsProtocol}//${window.location.host}/ws_pcm?token=${token}&device_name=webui-simple-recorder` - } else if (backendUrl != undefined && backendUrl != '') { - wsUrl = `${wsProtocol}//${backendUrl}/ws_pcm?token=${token}&device_name=webui-simple-recorder` - } - else if (port === '5173') { - // Development mode - wsUrl = `ws://localhost:8000/ws_pcm?token=${token}&device_name=webui-simple-recorder` - } else { - // Fallback - use same origin instead of hardcoded port 8000 - wsUrl = `${wsProtocol}//${window.location.host}/ws_pcm?token=${token}&device_name=webui-simple-recorder` - } + if (BACKEND_URL && BACKEND_URL.startsWith('http')) { + // BACKEND_URL is a full URL (e.g., http://localhost:8000) + const backendHost = BACKEND_URL.replace(/^https?:\/\//, '') + wsUrl = `${wsProtocol}//${backendHost}/ws_pcm?token=${token}&device_name=webui-simple-recorder` + } else if (BACKEND_URL && BACKEND_URL !== '') { + // BACKEND_URL is a path (e.g., /prod) + wsUrl = `${wsProtocol}//${window.location.host}${BACKEND_URL}/ws_pcm?token=${token}&device_name=webui-simple-recorder` } else { - // No environment variable set, use same origin as fallback - const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:' + // BACKEND_URL is empty (same origin) wsUrl = `${wsProtocol}//${window.location.host}/ws_pcm?token=${token}&device_name=webui-simple-recorder` } diff --git a/backends/advanced/webui/src/pages/Conversations.tsx b/backends/advanced/webui/src/pages/Conversations.tsx index b3a34b5c..d4b76ed3 100644 --- a/backends/advanced/webui/src/pages/Conversations.tsx +++ b/backends/advanced/webui/src/pages/Conversations.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react' import { MessageSquare, RefreshCw, Calendar, User, Play, Pause, MoreVertical, RotateCcw, Zap, ChevronDown, ChevronUp, Trash2 } from 'lucide-react' import { conversationsApi, BACKEND_URL } from '../services/api' import ConversationVersionHeader from '../components/ConversationVersionHeader' +import { getStorageKey } from '../utils/storage' interface Conversation { conversation_id?: string @@ -330,7 +331,7 @@ export default function Conversations() { // Check if we need to create a new audio element (none exists or previous had error) if (!audio || audio.error) { - const token = localStorage.getItem('token') || ''; + const token = localStorage.getItem(getStorageKey('token')) || ''; const audioUrl = `${BACKEND_URL}/api/audio/get_audio/${conversationId}?cropped=${useCropped}&token=${token}`; console.log('Creating audio element with URL:', audioUrl); console.log('Token present:', !!token, 'Token length:', token.length); @@ -647,7 +648,7 @@ export default function Conversations() { className="w-full h-10" preload="metadata" style={{ minWidth: '300px' }} - src={`${BACKEND_URL}/api/audio/get_audio/${conversation.conversation_id}?cropped=${!debugMode}&token=${localStorage.getItem('token') || ''}`} + src={`${BACKEND_URL}/api/audio/get_audio/${conversation.conversation_id}?cropped=${!debugMode}&token=${localStorage.getItem(getStorageKey('token')) || ''}`} > Your browser does not support the audio element. diff --git a/backends/advanced/webui/src/services/api.ts b/backends/advanced/webui/src/services/api.ts index 2617cdaa..6f6754f8 100644 --- a/backends/advanced/webui/src/services/api.ts +++ b/backends/advanced/webui/src/services/api.ts @@ -1,32 +1,52 @@ import axios from 'axios' +import { getStorageKey } from '../utils/storage' // Get backend URL from environment or auto-detect based on current location const getBackendUrl = () => { - // If explicitly set in environment, use that + const { protocol, hostname, port } = window.location + console.log('Protocol:', protocol) + console.log('Hostname:', hostname) + console.log('Port:', port) + + const isStandardPort = (protocol === 'https:' && (port === '' || port === '443')) || + (protocol === 'http:' && (port === '' || port === '80')) + + // Check if we have a base path (Caddy path-based routing) + const basePath = import.meta.env.BASE_URL + console.log('Base path from Vite:', basePath) + + if (isStandardPort && basePath && basePath !== '/') { + // We're using Caddy path-based routing - use the base path + console.log('Using Caddy path-based routing with base path') + return basePath.replace(/\/$/, '') + } + + // If explicitly set in environment, use that (for direct backend access) if (import.meta.env.VITE_BACKEND_URL !== undefined && import.meta.env.VITE_BACKEND_URL !== '') { + console.log('Using explicit VITE_BACKEND_URL') return import.meta.env.VITE_BACKEND_URL } - - // If accessed through proxy (standard ports), use relative URLs - const { protocol, hostname, port } = window.location - const isStandardPort = (protocol === 'https:' && (port === '' || port === '443')) || - (protocol === 'http:' && (port === '' || port === '80')) - + if (isStandardPort) { - // We're being accessed through nginx proxy or Kubernetes Ingress, use same origin - return '' // Empty string means use relative URLs (same origin) + // We're being accessed through nginx proxy or standard proxy + console.log('Using standard proxy - relative URLs') + return '' } - + // Development mode - direct access to dev server if (port === '5173') { + console.log('Development mode - using localhost:8000') return 'http://localhost:8000' } - + // Fallback + console.log('Fallback - using hostname:8000') return `${protocol}//${hostname}:8000` } const BACKEND_URL = getBackendUrl() +console.log('VITE_BACKEND_URL:', import.meta.env.VITE_BACKEND_URL) + console.log('๐ŸŒ API: Backend URL configured as:', BACKEND_URL || 'Same origin (relative URLs)') // Export BACKEND_URL for use in other components @@ -39,7 +59,7 @@ export const api = axios.create({ // Add request interceptor to include auth token api.interceptors.request.use((config) => { - const token = localStorage.getItem('token') + const token = localStorage.getItem(getStorageKey('token')) if (token) { config.headers.Authorization = `Bearer ${token}` } @@ -54,7 +74,7 @@ api.interceptors.response.use( if (error.response?.status === 401) { // Token expired or invalid, redirect to login console.warn('๐Ÿ” API: 401 Unauthorized - clearing token and redirecting to login') - localStorage.removeItem('token') + localStorage.removeItem(getStorageKey('token')) window.location.href = '/login' } else if (error.code === 'ECONNABORTED') { // Request timeout - don't logout, just log it @@ -227,7 +247,7 @@ export const chatApi = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('token')}` + 'Authorization': `Bearer ${localStorage.getItem(getStorageKey('token'))}` }, body: JSON.stringify(requestBody) }) diff --git a/backends/advanced/webui/src/utils/storage.ts b/backends/advanced/webui/src/utils/storage.ts new file mode 100644 index 00000000..24c5c184 --- /dev/null +++ b/backends/advanced/webui/src/utils/storage.ts @@ -0,0 +1,11 @@ +/** + * Helper to get environment-specific localStorage keys + * Each environment (dev, test, test2, etc.) gets its own token storage + * This prevents token conflicts when running multiple environments simultaneously + */ +export const getStorageKey = (key: string): string => { + const basePath = import.meta.env.BASE_URL || '/' + // Normalize: /test2/ -> test2, / -> root + const envName = basePath.replace(/^\/|\/$/g, '') || 'root' + return `${envName}_${key}` +} diff --git a/backends/advanced/webui/vite.config.ts b/backends/advanced/webui/vite.config.ts index 8bad98b0..5d85f3de 100644 --- a/backends/advanced/webui/vite.config.ts +++ b/backends/advanced/webui/vite.config.ts @@ -3,10 +3,11 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], + base: process.env.VITE_BASE_PATH || '/', server: { port: 5173, host: '0.0.0.0', - allowedHosts: process.env.VITE_ALLOWED_HOSTS + allowedHosts: process.env.VITE_ALLOWED_HOSTS ? process.env.VITE_ALLOWED_HOSTS.split(' ').map(host => host.trim()).filter(host => host.length > 0) : [ 'localhost', From 059def29f621f45a4c03ba3865dad7789776bb76 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Tue, 9 Dec 2025 22:20:31 +0000 Subject: [PATCH 18/21] support new comcker include --- backends/advanced/docker-compose.yml | 15 +++++++-------- backends/advanced/webui/Dockerfile | 7 +++++-- backends/advanced/webui/nginx.conf | 4 ++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/backends/advanced/docker-compose.yml b/backends/advanced/docker-compose.yml index d9f3eb51..b4a54c73 100644 --- a/backends/advanced/docker-compose.yml +++ b/backends/advanced/docker-compose.yml @@ -9,23 +9,22 @@ # Production: docker compose -f docker-compose.yml -f compose/overrides/prod.yml up # # Structure: -# compose/infrastructure.yml - Core services (mongo, redis, qdrant) # compose/backend.yml - Backend API and workers # compose/frontend.yml - Web UI -# compose/mycelia.yml - Mycelia services (--profile mycelia) -# compose/optional-services.yml - Caddy, Ollama, etc. # compose/overrides/ - Environment-specific overrides include: - # Core infrastructure (always included) - - compose/infrastructure.yml - # Application services (always included) - compose/backend.yml - compose/frontend.yml - # Optional services (profile-based) - - compose/optional-services.yml + # Note: Infrastructure (MongoDB, Redis, Qdrant) is SHARED across all environments + # Start infrastructure once with: docker compose -f ../../compose/infrastructure-shared.yml up -d + # Each environment connects to shared infrastructure using unique database names + + # Note: Caddy is SHARED across all environments + # Start Caddy once with: docker compose -f ../../compose/caddy.yml up -d + # Caddy serves all environments via path-based routing # Note: Mycelia moved to root level (../../compose/mycelia.yml) # To use Mycelia, run from project root: docker compose --profile mycelia up diff --git a/backends/advanced/webui/Dockerfile b/backends/advanced/webui/Dockerfile index b4eb8cad..fcfc2f6d 100644 --- a/backends/advanced/webui/Dockerfile +++ b/backends/advanced/webui/Dockerfile @@ -17,8 +17,11 @@ COPY . . ARG VITE_ALLOWED_HOSTS ENV VITE_ALLOWED_HOSTS=${VITE_ALLOWED_HOSTS} -ARG VITE_BACKEND_URL -ENV VITE_BACKEND_URL=${VITE_BACKEND_URL} +ARG VITE_BASE_PATH +ENV VITE_BASE_PATH=${VITE_BASE_PATH:-/} + +# Debug: Print BASE_PATH value (forces cache invalidation when changed) +RUN echo "Building with VITE_BASE_PATH=${VITE_BASE_PATH}" # Build the application RUN npm run build diff --git a/backends/advanced/webui/nginx.conf b/backends/advanced/webui/nginx.conf index f9c6f0ed..0ec05aea 100644 --- a/backends/advanced/webui/nginx.conf +++ b/backends/advanced/webui/nginx.conf @@ -26,6 +26,10 @@ server { try_files $uri $uri/ /index.html; } + # Add base tag for path-based routing (if X-Forwarded-Prefix header is present) + sub_filter '' ''; + sub_filter_once on; + # Cache static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; From 0c3dc4a88e5d7ee3dd40e994a4f7a209290710b2 Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Tue, 9 Dec 2025 22:26:02 +0000 Subject: [PATCH 19/21] add and moved docs --- .gitignore | 24 + Docs/development/DOCKER-SETUP-SUMMARY.md | 265 + Docs/development/README.md | 40 + Docs/development/SETUP_COMPLETE.md | 428 ++ Docs/development/SETUP_WIZARD_SUMMARY.md | 357 ++ Docs/setup/advanced/CADDY_SETUP.md | 328 + .../setup/advanced/ENVIRONMENTS.md | 4 +- .../MULTI_ENVIRONMENT_ARCHITECTURE.md | 406 ++ Docs/setup/advanced/README.md | 64 + Docs/setup/advanced/SSL_SETUP.md | 467 ++ Docs/setup/advanced/TAILSCALE-SERVE-GUIDE.md | 215 + Docs/setup/advanced/TAILSCALE_GUIDE.md | 281 + INSTALL.md | 9 +- SETUP.md | 218 - backends/advanced/Docs/README.md | 17 +- chronicler.excalidraw | 5646 +++++++++++++++++ 16 files changed, 8544 insertions(+), 225 deletions(-) create mode 100644 Docs/development/DOCKER-SETUP-SUMMARY.md create mode 100644 Docs/development/README.md create mode 100644 Docs/development/SETUP_COMPLETE.md create mode 100644 Docs/development/SETUP_WIZARD_SUMMARY.md create mode 100644 Docs/setup/advanced/CADDY_SETUP.md rename ENVIRONMENTS.md => Docs/setup/advanced/ENVIRONMENTS.md (98%) create mode 100644 Docs/setup/advanced/MULTI_ENVIRONMENT_ARCHITECTURE.md create mode 100644 Docs/setup/advanced/README.md create mode 100644 Docs/setup/advanced/SSL_SETUP.md create mode 100644 Docs/setup/advanced/TAILSCALE-SERVE-GUIDE.md create mode 100644 Docs/setup/advanced/TAILSCALE_GUIDE.md delete mode 100644 SETUP.md create mode 100644 chronicler.excalidraw diff --git a/.gitignore b/.gitignore index 45bd86ba..b4c1db50 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ backends/advanced-backend/data/speaker_model_cache/ backends/charts/advanced-backend/env-configmap.yaml extras/openmemory-mcp/data/* +extras/openmemory/data/* .env.backup.* backends/advanced/nginx.conf @@ -86,3 +87,26 @@ output.xml report.html .secrets extras/openmemory-mcp/.env.openmemory +extras/openmemory/.env +certs + +# Environment-specific configuration files (added 2025-12-09) +environments/ +*.env.backup.* +backends/advanced/.env.* +!backends/advanced/.env.template + +# SSL certificates +*.crt +*.key + +# IDE and tool directories +.playwright-mcp/ +.serena/ + +# Docker compose data directories +**/compose/data/ + +# Deprecated compose files (moved to root compose/) +backends/advanced/compose/infrastructure.yml +backends/advanced/compose/mycelia.yml diff --git a/Docs/development/DOCKER-SETUP-SUMMARY.md b/Docs/development/DOCKER-SETUP-SUMMARY.md new file mode 100644 index 00000000..823009ef --- /dev/null +++ b/Docs/development/DOCKER-SETUP-SUMMARY.md @@ -0,0 +1,265 @@ +# Docker Compose Setup Summary + +## โœ… What Was Created + +### Root Level (Project-Wide Management) + +``` +/Users/stu/repos/friend-lite/ # PROJECT ROOT +โ”œโ”€โ”€ docker-compose.yml # โญ NEW: Unified root compose +โ”œโ”€โ”€ DOCKER-COMPOSE.md # โญ NEW: Complete documentation +โ””โ”€โ”€ compose/ # โญ NEW: Modular service definitions + โ”œโ”€โ”€ advanced-backend.yml # Includes backends/advanced/ + โ”œโ”€โ”€ asr-services.yml # Offline ASR (Parakeet) + โ”œโ”€โ”€ speaker-recognition.yml # Voice identification + โ”œโ”€โ”€ openmemory.yml # OpenMemory MCP server + โ””โ”€โ”€ observability.yml # Langfuse monitoring +``` + +### Backend Level (Backend-Specific Services) + +``` +backends/advanced/ +โ”œโ”€โ”€ docker-compose.yml # โญ UPDATED: Now uses includes +โ”œโ”€โ”€ DOCKER-COMPOSE-GUIDE.md # โญ NEW: Backend-specific docs +โ”œโ”€โ”€ docker-compose.yml.backup # โญ BACKUP: Original monolithic file +โ””โ”€โ”€ compose/ # โญ NEW: Modular backend services + โ”œโ”€โ”€ infrastructure.yml # Mongo, Redis, Qdrant + โ”œโ”€โ”€ backend.yml # Friend-backend, Workers + โ”œโ”€โ”€ frontend.yml # WebUI + โ”œโ”€โ”€ mycelia.yml # Mycelia (--profile mycelia) + โ”œโ”€โ”€ optional-services.yml # Caddy, Ollama, etc. + โ””โ”€โ”€ overrides/ + โ”œโ”€โ”€ dev.yml # Development settings + โ”œโ”€โ”€ test.yml # Test environment + โ””โ”€โ”€ prod.yml # Production config +``` + +## ๐ŸŽฏ How to Use + +### From Project Root (Recommended) + +```bash +cd /path/to/friend-lite + +# Basic development +docker compose up + +# With optional services +docker compose --profile mycelia up +docker compose --profile speaker up +docker compose --profile asr up + +# Multiple profiles +docker compose --profile mycelia --profile speaker up + +# Testing +docker compose -f docker-compose.yml -f backends/advanced/compose/overrides/test.yml up + +# Production +docker compose -f docker-compose.yml -f backends/advanced/compose/overrides/prod.yml up +``` + +### From backends/advanced/ (Still Works) + +```bash +cd backends/advanced + +# Development +docker compose up + +# With Mycelia +docker compose --profile mycelia up + +# Testing +docker compose -f docker-compose.yml -f compose/overrides/test.yml up +``` + +## ๐Ÿ“Š Key Improvements + +### Environment Variables + +**Before:** +```yaml +services: + friend-backend: + env_file: .env + environment: + - DEEPGRAM_API_KEY=${DEEPGRAM_API_KEY} # โŒ Redundant + - OPENAI_API_KEY=${OPENAI_API_KEY} # โŒ Redundant + - MISTRAL_API_KEY=${MISTRAL_API_KEY} # โŒ Redundant + # ... 37 more redundant lines +``` + +**After:** +```yaml +services: + friend-backend: + env_file: ../.env # โœ… All variables loaded automatically + environment: + # โœ… Only Docker-specific overrides + - REDIS_URL=redis://redis:6379/0 + - MYCELIA_URL=${MYCELIA_URL:-http://mycelia-backend:5173} +``` + +**Reduction:** From 40+ variables to 3 variables per service (92% reduction!) + +### File Organization + +**Before:** +- `docker-compose.yml` - 343 lines, everything in one file +- `docker-compose-test.yml` - 248 lines, duplicated config +- Multiple scattered compose files across extras/ + +**After:** +- Root `docker-compose.yml` - 51 lines (includes only) +- Backend `docker-compose.yml` - 55 lines (includes only) +- Modular files - 20-80 lines each, focused purpose + +**Result:** 88% reduction in root file size, better organization + +## ๐Ÿ”ง What Changed + +### 1. Root-Level Unified Control + +**Old way:** +```bash +# Start backend +cd backends/advanced && docker compose up + +# In another terminal, start speaker recognition +cd extras/speaker-recognition && docker compose up + +# In another terminal, start ASR +cd extras/asr-services && docker compose up +``` + +**New way:** +```bash +# Everything from project root +cd /path/to/friend-lite +docker compose --profile speaker --profile asr up +``` + +### 2. Clean Environment Configuration + +All API keys and secrets in `backends/advanced/.env` are automatically loaded. No need to list them in docker-compose.yml unless: +- Providing a default value: `${VAR:-default}` +- Overriding for Docker networking: `REDIS_URL=redis://redis:6379/0` +- Setting service-specific values: `CORS_ORIGINS=...` + +### 3. Modular Service Definitions + +Services grouped by purpose: +- **Infrastructure** (mongo, redis, qdrant) +- **Backend** (friend-backend, workers) +- **Frontend** (webui) +- **Optional** (mycelia, caddy, ollama) + +### 4. Environment Switching + +```bash +# Development (default) +docker compose up + +# Test (isolated ports and databases) +docker compose -f docker-compose.yml -f backends/advanced/compose/overrides/test.yml up + +# Production (no source mounts, resource limits) +docker compose -f docker-compose.yml -f backends/advanced/compose/overrides/prod.yml up +``` + +## ๐Ÿ“ฆ Service Profiles + +| Profile | Services Added | Command | +|---------|---------------|---------| +| *(default)* | Core backend, WebUI, databases | `docker compose up` | +| `mycelia` | Mycelia memory service | `docker compose --profile mycelia up` | +| `speaker` | Speaker recognition | `docker compose --profile speaker up` | +| `asr` | Parakeet offline ASR | `docker compose --profile asr up` | +| `openmemory` | OpenMemory MCP server | `docker compose --profile openmemory up` | +| `observability` | Langfuse monitoring | `docker compose --profile observability up` | +| `https` | Caddy reverse proxy | `docker compose --profile https up` | + +## ๐Ÿš€ Quick Commands + +```bash +# View merged configuration +docker compose config + +# List services (default) +docker compose config --services + +# List services with profile +docker compose --profile mycelia config --services + +# Start specific services only +docker compose up mongo redis qdrant + +# View logs +docker compose logs -f friend-backend + +# Rebuild +docker compose build +docker compose up --build + +# Stop everything +docker compose down + +# Reset data (โš ๏ธ CAUTION) +docker compose down -v +sudo rm -rf backends/advanced/data/ +``` + +## ๐ŸŽ“ Migration Notes + +### If You Were Using backends/advanced/docker-compose.yml + +**Nothing breaks!** The old way still works: + +```bash +cd backends/advanced +docker compose up # Still works exactly the same +``` + +**New recommended way:** + +```bash +cd /path/to/friend-lite # Project root +docker compose up # Same result, unified control +``` + +### Reverting to Old Structure + +If you need to revert: + +```bash +cd backends/advanced +mv docker-compose.yml docker-compose-modular.yml +mv docker-compose.yml.backup docker-compose.yml +``` + +## ๐Ÿ“š Documentation + +- **[DOCKER-COMPOSE.md](DOCKER-COMPOSE.md)** - Complete guide to root-level compose +- **[backends/advanced/DOCKER-COMPOSE-GUIDE.md](backends/advanced/DOCKER-COMPOSE-GUIDE.md)** - Backend-specific details +- **[CLAUDE.md](CLAUDE.md)** - Project overview and development commands + +## โœจ Benefits Summary + +1. **Single Entry Point** - Always start from project root +2. **Unified Control** - One command manages all services +3. **Modular Design** - Services organized by purpose +4. **Clean Configs** - 92% reduction in redundant env vars +5. **Easy Switching** - Dev/test/prod via simple flags +6. **Optional Services** - Enable only what you need via profiles +7. **Better Documentation** - Clear guides for each level + +## ๐ŸŽ‰ Result + +You can now manage your entire Friend-Lite stack from a single location with clean, modular configuration files! + +```bash +# This one command can start everything: +docker compose --profile mycelia --profile speaker --profile asr up +``` diff --git a/Docs/development/README.md b/Docs/development/README.md new file mode 100644 index 00000000..066070c6 --- /dev/null +++ b/Docs/development/README.md @@ -0,0 +1,40 @@ +# Development Documentation + +This directory contains documentation about Chronicle's development and internal systems. + +## Available Documentation + +- **[SETUP_COMPLETE.md](SETUP_COMPLETE.md)** - Setup system architecture + - Multi-environment configuration system + - How the setup wizard works + - Docker Compose and Kubernetes support + +- **[SETUP_WIZARD_SUMMARY.md](SETUP_WIZARD_SUMMARY.md)** - Setup wizard documentation + - Wizard implementation details + - Environment configuration + - Secrets management + +- **[DOCKER-SETUP-SUMMARY.md](DOCKER-SETUP-SUMMARY.md)** - Docker setup architecture + - Container organization + - Volume management + - Network configuration + +## Audience + +These documents are for: + +- **Contributors** working on Chronicle's setup system +- **Developers** extending the wizard or configuration +- **Advanced users** who want to understand internals + +## For Regular Users + +If you're looking to **install or use Chronicle**, see: + +- **[INSTALL.md](../../INSTALL.md)** - Installation guide +- **[CLAUDE.md](../../CLAUDE.md)** - Main documentation +- **[Docs/setup/](../setup/)** - Setup guides + +--- + +**Contributing?** See the main [Chronicle repository](https://github.com/BasedHardware/Friend) diff --git a/Docs/development/SETUP_COMPLETE.md b/Docs/development/SETUP_COMPLETE.md new file mode 100644 index 00000000..6a5498f7 --- /dev/null +++ b/Docs/development/SETUP_COMPLETE.md @@ -0,0 +1,428 @@ +# Setup System Complete! ๐ŸŽ‰ + +## What We Built + +A comprehensive, production-ready setup system for Friend-Lite that handles: +- โœ… Multi-environment configuration +- โœ… Secrets management +- โœ… Tailscale integration +- โœ… SSL/TLS configuration +- โœ… Interactive wizard +- โœ… Docker Compose and Kubernetes support + +## Quick Start + +### For New Users + +```bash +# Run the interactive wizard +make wizard +``` + +That's it! The wizard guides you through everything. + +### For Existing Users + +Your existing setup still works! No breaking changes. + +```bash +# Old way still works +./start-env.sh dev + +# New way with wizard-created environments +make wizard +./start-env.sh +``` + +## What Changed + +### New Files Created + +1. **`WIZARD.md`** - Complete wizard documentation +2. **`SSL_SETUP.md`** - SSL/TLS configuration guide +3. **`SETUP_WIZARD_SUMMARY.md`** - Implementation details +4. **`SKAFFOLD_INTEGRATION.md`** - Skaffold/K8s integration +5. **`SETUP_COMPLETE.md`** (this file) - Quick reference + +### Files Modified + +1. **`Makefile`** - Added wizard targets (`make wizard`, `make setup-secrets`, etc.) +2. **`config-docker.env`** - Added SSL/TLS configuration variables +3. **`config-k8s.env`** - Added SSL/TLS configuration variables +4. **`CLAUDE.md`** - Added Quick Setup section + +### Files Preserved + +All existing configuration files remain unchanged: +- โœ… `docker-defaults.env` - System defaults +- โœ… `start-env.sh` - Environment starter script +- โœ… `.env.secrets.template` - Secrets template +- โœ… Existing environment files in `environments/` + +## Configuration System Overview + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Configuration Layers โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ 1. docker-defaults.env System infrastructure โ”‚ +โ”‚ โ€ข MongoDB/Redis/Qdrant URLs โ”‚ +โ”‚ โ€ข Service names โ”‚ +โ”‚ โ€ข Rarely changed โ”‚ +โ”‚ โ”‚ +โ”‚ 2. config-docker.env User settings โ”‚ +โ”‚ โ€ข LLM provider (OpenAI, Ollama) โ”‚ +โ”‚ โ€ข Transcription provider (Deepgram, Parakeet) โ”‚ +โ”‚ โ€ข Feature flags โ”‚ +โ”‚ โ€ข SSL/TLS settings (NEW) โ”‚ +โ”‚ โ”‚ +โ”‚ 3. .env.secrets Sensitive credentials โ”‚ +โ”‚ โ€ข API keys (OpenAI, Deepgram, Mistral) โ”‚ +โ”‚ โ€ข JWT secret โ”‚ +โ”‚ โ€ข Admin password โ”‚ +โ”‚ โ€ข Gitignored, wizard-created (NEW) โ”‚ +โ”‚ โ”‚ +โ”‚ 4. environments/.env Environment-specific โ”‚ +โ”‚ โ€ข Port offset โ”‚ +โ”‚ โ€ข Database names โ”‚ +โ”‚ โ€ข Optional services โ”‚ +โ”‚ โ€ข Tailscale hostname (NEW) โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## SSL/TLS Options + +### Option 1: No SSL (Local Development) +```bash +# In config-docker.env +HTTPS_ENABLED=false + +# Just start services +./start-env.sh dev +``` + +### Option 2: Caddy (Browser Microphone Access) +```bash +# Start with Caddy profile +./start-env.sh dev --profile https + +# Access at https://localhost (accept self-signed warning) +``` + +### Option 3: Tailscale Serve (Production) +```bash +# Run wizard and choose Tailscale + option 1 +make wizard + +# Start services +./start-env.sh prod + +# Enable Tailscale HTTPS +tailscale serve https / http://localhost:8000 +tailscale serve https / http://localhost:5173 + +# Access at https://your-hostname.tailxxxxx.ts.net (no warnings!) +``` + +### Option 4: Self-Signed Certificates +```bash +# Generate certificates +cd backends/advanced +./ssl/generate-ssl.sh friend-lite.tailxxxxx.ts.net + +# Configure in config-docker.env or environment file +HTTPS_ENABLED=true +SSL_CERT_PATH=./ssl/server.crt +SSL_KEY_PATH=./ssl/server.key +TAILSCALE_HOSTNAME=friend-lite.tailxxxxx.ts.net +``` + +## Makefile Commands + +### Setup Commands + +```bash +make wizard # ๐Ÿง™ Full interactive setup wizard +make setup-secrets # ๐Ÿ” Configure API keys and passwords +make setup-tailscale # ๐ŸŒ Configure Tailscale and SSL +make setup-environment # ๐Ÿ“ฆ Create environment config +make check-secrets # โœ… Validate secrets file +``` + +### Existing Commands (Still Work) + +```bash +make config-docker # Generate Docker Compose configs +make config-k8s # Generate Kubernetes configs +make deploy-docker # Deploy with Docker Compose +make deploy-k8s # Deploy to Kubernetes +make setup-dev # Setup git hooks and pre-commit +make test-robot # Run all Robot Framework tests +``` + +### Quick Reference + +```bash +make # Show main menu +make help # Show detailed help +make menu # Show main menu +``` + +## Environment Management + +### Create Environments + +```bash +# Create dev environment (port offset 0) +make setup-environment +# Enter: dev, offset 0 + +# Create staging environment (port offset 100) +make setup-environment +# Enter: staging, offset 100 + +# Create prod environment (port offset 200) +make setup-environment +# Enter: prod, offset 200 +``` + +### Start Environments + +```bash +# Start single environment +./start-env.sh dev + +# Start with optional services +./start-env.sh dev --profile mycelia +./start-env.sh dev --profile speaker +./start-env.sh dev --profile openmemory + +# Multiple profiles +./start-env.sh dev --profile mycelia --profile speaker + +# Run multiple environments simultaneously +./start-env.sh dev & +./start-env.sh staging & +./start-env.sh prod & +``` + +### Environment Structure + +Each environment is isolated: +``` +dev: ports 8000-8099, database friend-lite-dev, data ./data/dev +staging: ports 8100-8199, database friend-lite-staging, data ./data/staging +prod: ports 8200-8299, database friend-lite-prod, data ./data/prod +``` + +## Distributed Deployment with Tailscale + +### Setup + +1. **Install Tailscale on all machines:** + ```bash + curl -fsSL https://tailscale.com/install.sh | sh + sudo tailscale up + ``` + +2. **Run wizard on each machine:** + ```bash + make wizard + # Choose Tailscale setup + # Each machine gets its own hostname + ``` + +3. **Configure service URLs:** + ```bash + # On backend machine - config-docker.env + SPEAKER_SERVICE_URL=https://speaker.tailxxxxx.ts.net:8085 + + # On speaker machine - config-docker.env + BACKEND_URL=https://backend.tailxxxxx.ts.net:8000 + ``` + +4. **Services automatically discover each other via Tailscale!** + +## Kubernetes Deployment + +### Generate Configs + +```bash +# Ensure secrets are configured +make setup-secrets + +# Generate K8s ConfigMaps and Secrets +make config-k8s +``` + +This creates: +- `k8s-manifests/configmap.yaml` - Non-sensitive config from `config-k8s.env` +- `k8s-manifests/secrets.yaml` - Sensitive config from `.env.secrets` + +### Deploy + +```bash +# Deploy with Skaffold +make deploy-k8s + +# Or manually +kubectl apply -f k8s-manifests/configmap.yaml +kubectl apply -f k8s-manifests/secrets.yaml +skaffold run +``` + +## Troubleshooting + +### Issue: "make: command not found" + +**Solution:** Install make +```bash +# macOS +xcode-select --install + +# Ubuntu/Debian +sudo apt-get install build-essential + +# Windows +# Install via WSL or use Git Bash +``` + +### Issue: "openssl: command not found" + +**Solution:** Install OpenSSL +```bash +# macOS +brew install openssl + +# Ubuntu/Debian +sudo apt-get install openssl +``` + +### Issue: ".env.secrets already exists" + +**Solution:** Wizard asks before overwriting +```bash +# Reconfigure existing secrets +make setup-secrets +# Answer "y" when prompted + +# Or manually edit +nano .env.secrets +``` + +### Issue: "Port already in use" + +**Solution:** Use different port offset +```bash +# Create new environment with different offset +make setup-environment +# Enter offset: 100 (or 200, 300, etc.) +``` + +### Issue: "Tailscale not found" + +**Solution:** Install Tailscale +```bash +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up +``` + +## Migration from Old Setup + +If you have existing configuration: + +1. **Secrets**: Move API keys to `.env.secrets` + ```bash + make setup-secrets + # Enter your existing API keys + ``` + +2. **Environments**: Convert existing `.env` to environment files + ```bash + make setup-environment + # Enter same values as your current .env + ``` + +3. **No breaking changes**: Old setup still works! + ```bash + # Still works + ./start-env.sh dev + ``` + +## Documentation + +| File | Purpose | +|------|---------| +| `WIZARD.md` | Complete wizard documentation | +| `SSL_SETUP.md` | SSL/TLS configuration guide | +| `ENVIRONMENTS.md` | Environment system details | +| `SETUP.md` | Complete setup guide | +| `CLAUDE.md` | Development guide (includes Quick Setup) | +| `SETUP_WIZARD_SUMMARY.md` | Implementation details | +| `SKAFFOLD_INTEGRATION.md` | Kubernetes/Skaffold integration | + +## Testing the Setup + +### Quick Test + +```bash +# Run wizard +make wizard +# Provide test values: +# - JWT secret: press Enter (auto-generate) +# - Admin email: test@example.com +# - Admin password: testpassword +# - OpenAI key: sk-test... +# - Tailscale: N +# - Environment: test, offset 300 + +# Start test environment +./start-env.sh test + +# Check services +curl http://localhost:8300/health + +# Cleanup +docker compose -p friend-lite-test down -v +rm environments/test.env +``` + +## Summary + +You now have a production-ready setup system with: + +โœ… **Interactive wizard** - `make wizard` for guided setup +โœ… **Secrets management** - `.env.secrets` for API keys (gitignored) +โœ… **Multi-environment** - Run dev/staging/prod simultaneously +โœ… **Tailscale integration** - Distributed deployments with automatic HTTPS +โœ… **SSL/TLS support** - Four options (no SSL, Caddy, Tailscale, self-signed) +โœ… **Kubernetes ready** - ConfigMap/Secret generation from configs +โœ… **Backward compatible** - Existing setups continue to work +โœ… **Comprehensive docs** - WIZARD.md, SSL_SETUP.md, and more + +## Next Steps + +1. **Try the wizard:** + ```bash + make wizard + ``` + +2. **Start your environment:** + ```bash + ./start-env.sh + ``` + +3. **Explore documentation:** + ```bash + cat WIZARD.md + cat SSL_SETUP.md + ``` + +4. **Deploy to production:** + - Use `make wizard` with Tailscale + - Choose "Tailscale Serve" for automatic HTTPS + - Access via your Tailscale hostname + +๐ŸŽ‰ **You're all set! Enjoy Friend-Lite!** diff --git a/Docs/development/SETUP_WIZARD_SUMMARY.md b/Docs/development/SETUP_WIZARD_SUMMARY.md new file mode 100644 index 00000000..a794e064 --- /dev/null +++ b/Docs/development/SETUP_WIZARD_SUMMARY.md @@ -0,0 +1,357 @@ +# Setup Wizard Implementation Summary + +## What Was Built + +A comprehensive, interactive setup wizard integrated into the Friend-Lite Makefile that guides users through complete configuration in a single command: `make wizard`. + +## Key Features + +### 1. Interactive Setup Wizard (`make wizard`) +Single command that orchestrates all setup steps: +- Secrets configuration +- Tailscale setup (optional) +- Environment creation +- Clear next steps + +### 2. Modular Components +Each step can be run independently: +- `make setup-secrets` - API keys and passwords +- `make setup-tailscale` - Distributed deployment configuration +- `make setup-environment` - Create isolated environments +- `make check-secrets` - Validate secrets configuration + +### 3. Smart Validations +- Checks for existing files before overwriting +- Creates timestamped backups automatically +- Validates Tailscale installation and status +- Auto-detects Tailscale hostnames +- Generates secure JWT keys automatically + +### 4. SSL/TLS Integration +Three SSL options integrated into wizard: +1. **Tailscale Serve** - Automatic HTTPS (recommended) +2. **Self-signed certificates** - Generated for Tailscale hostname +3. **Skip SSL** - HTTP only for development + +### 5. Environment Isolation +Creates fully isolated environments with: +- Unique port offsets (no conflicts) +- Separate databases per environment +- Custom data directories +- Optional service selection +- Tailscale/SSL configuration per environment + +## Files Created/Modified + +### New Files + +1. **`WIZARD.md`** (2.8KB) + - Complete wizard documentation + - Example workflows + - Troubleshooting guide + - Configuration reference + +2. **`SSL_SETUP.md`** (14KB) + - SSL/TLS architecture explanation + - Three setup methods (Caddy, self-signed, Tailscale) + - Service-specific SSL configuration + - CORS and certificate management + +3. **`SETUP_WIZARD_SUMMARY.md`** (this file) + - Implementation overview + - Quick reference guide + +### Modified Files + +1. **`Makefile`** (+400 lines) + - Added `wizard` target (main entry point) + - Added `setup-secrets` target (secrets configuration) + - Added `setup-tailscale` target (Tailscale integration) + - Added `setup-environment` target (environment creation) + - Added `check-secrets` target (validation) + - Updated menu with wizard section + - Updated `.PHONY` declarations + +2. **`config-docker.env`** (+15 lines) + - Added SSL/TLS configuration section + - Added `HTTPS_ENABLED` flag + - Added `SSL_CERT_PATH` and `SSL_KEY_PATH` + - Added `TAILSCALE_HOSTNAME` variable + +3. **`config-k8s.env`** (+12 lines) + - Added SSL/TLS configuration section + - Added `HTTPS_ENABLED` flag + - Added `SSL_CERT_SECRET` for K8s TLS secret + - Added `TAILSCALE_HOSTNAME` variable + +4. **`SKAFFOLD_INTEGRATION.md`** (updated) + - Documented Makefile secrets loading + - Explained K8s ConfigMap/Secret generation + +## Configuration Flow + +``` +make wizard + โ”‚ + โ”œโ”€> make setup-secrets + โ”‚ โ”œโ”€> Create .env.secrets from template + โ”‚ โ”œโ”€> Prompt for JWT secret (or auto-generate) + โ”‚ โ”œโ”€> Prompt for admin credentials + โ”‚ โ”œโ”€> Prompt for API keys (OpenAI, Deepgram, Mistral, HF) + โ”‚ โ””โ”€> Save to .env.secrets (gitignored) + โ”‚ + โ”œโ”€> make setup-tailscale (optional) + โ”‚ โ”œโ”€> Check Tailscale installation + โ”‚ โ”œโ”€> Check Tailscale running status + โ”‚ โ”œโ”€> List Tailscale devices + โ”‚ โ”œโ”€> Auto-detect Tailscale hostname + โ”‚ โ”œโ”€> Prompt for SSL method: + โ”‚ โ”‚ โ”œโ”€> Option 1: Tailscale Serve (automatic HTTPS) + โ”‚ โ”‚ โ”œโ”€> Option 2: Generate self-signed certs + โ”‚ โ”‚ โ””โ”€> Option 3: Skip SSL + โ”‚ โ””โ”€> Export TAILSCALE_HOSTNAME and HTTPS_ENABLED + โ”‚ + โ””โ”€> make setup-environment + โ”œโ”€> List existing environments + โ”œโ”€> Prompt for environment name (default: dev) + โ”œโ”€> Prompt for port offset (default: 0) + โ”œโ”€> Prompt for database names + โ”œโ”€> Prompt for optional services: + โ”‚ โ”œโ”€> Mycelia + โ”‚ โ”œโ”€> Speaker Recognition + โ”‚ โ”œโ”€> OpenMemory MCP + โ”‚ โ””โ”€> Parakeet ASR + โ”œโ”€> Include Tailscale config (if set) + โ””โ”€> Write environments/.env +``` + +## Usage Examples + +### Example 1: Quick Local Setup +```bash +make wizard +# 1. Configure secrets โ†’ Yes +# 2. Tailscale โ†’ No +# 3. Environment: dev, port offset: 0 +# Result: ./start-env.sh dev +``` + +### Example 2: Production with Tailscale +```bash +make wizard +# 1. Configure secrets โ†’ Yes +# 2. Tailscale โ†’ Yes, option 1 (Tailscale Serve) +# 3. Environment: prod, port offset: 0, services: mycelia+speaker +# Result: ./start-env.sh prod +# tailscale serve https / http://localhost:8000 +``` + +### Example 3: Multiple Environments +```bash +make wizard # Create dev (offset 0) +make setup-environment # Create staging (offset 100) +make setup-environment # Create prod (offset 200) + +./start-env.sh dev & +./start-env.sh staging & +./start-env.sh prod & +``` + +### Example 4: Individual Steps +```bash +# Just configure secrets +make setup-secrets + +# Just setup Tailscale +make setup-tailscale + +# Just create an environment +make setup-environment + +# Check secrets are valid +make check-secrets +``` + +## Technical Implementation Details + +### Makefile Techniques Used + +1. **Variable Scoping**: Uses `$$variable` for shell variables within make targets +2. **Conditional Logic**: Uses `if [ condition ]; then ... fi` for branching +3. **Default Values**: Uses `$${var:-default}` for optional prompts +4. **Exit Codes**: Uses `exit 0` for graceful skipping, `exit 1` for errors +5. **Target Dependencies**: `wizard` calls sub-targets with `$(MAKE)` +6. **Silent Mode**: Uses `@` prefix for clean output +7. **Environment Export**: Uses `export` for passing variables between targets + +### Security Considerations + +1. **Secrets Isolation**: `.env.secrets` is gitignored +2. **Backup System**: Timestamped backups before overwriting +3. **Password Masking**: Uses `read -sp` for password input +4. **Random Generation**: Uses `openssl rand` for JWT keys +5. **Certificate Validity**: Self-signed certs valid for 365 days +6. **File Permissions**: Sets appropriate permissions on generated files + +### Integration Points + +1. **start-env.sh**: Loads secrets and environment files +2. **docker-compose.yml**: Uses variables for SSL configuration +3. **Caddyfile**: Generated with Tailscale hostname support +4. **SSL Scripts**: `ssl/generate-ssl.sh` for certificate generation +5. **Skaffold**: Makefile loads secrets for K8s ConfigMap/Secret generation + +## Benefits Over Previous System + +| Aspect | Before | After | +|--------|--------|-------| +| Secrets Management | Manual .env editing | Interactive prompts with validation | +| Tailscale Setup | Manual scripts | Integrated wizard with detection | +| SSL Configuration | Separate scripts | Three options in wizard | +| Environment Creation | Manual file creation | Interactive prompts with defaults | +| Backup Safety | Manual backups | Automatic timestamped backups | +| Validation | None | Built-in checks and error handling | +| Documentation | Scattered | Centralized in WIZARD.md | +| User Experience | Multiple steps | Single command | + +## Comparison to Python/Shell Scripts + +### Why Makefile is Better + +**Advantages:** +โœ… Standard tooling (available everywhere) +โœ… Declarative targets (self-documenting) +โœ… Dependency management built-in +โœ… Idempotent by design +โœ… Easy to extend with new targets +โœ… Integrates with existing build system +โœ… No Python dependencies needed +โœ… Users can see all available commands with `make` + +**Makefile:** +```bash +make wizard # Clear, simple +make setup-secrets # Modular steps +make help # Self-documenting +``` + +**Python Script:** +```bash +python wizard.py # Requires Python +./wizard.py # Shebang issues +pip install -r requirements.txt # Dependencies +``` + +**Shell Script:** +```bash +./wizard.sh # Not self-documenting +./setup-secrets.sh # Multiple scripts +./wizard.sh --help # Manual help implementation +``` + +## Quick Reference + +### Main Commands + +```bash +make wizard # Full interactive setup +make setup-secrets # Configure API keys and passwords +make setup-tailscale # Configure Tailscale and SSL +make setup-environment # Create environment config +make check-secrets # Validate secrets file +make menu # Show all available commands +``` + +### After Setup + +```bash +./start-env.sh # Start environment +make config-k8s # Generate K8s configs (if using K8s) +make deploy-docker # Deploy with Docker Compose +``` + +### Documentation + +```bash +cat WIZARD.md # Wizard documentation +cat SSL_SETUP.md # SSL/TLS configuration +cat ENVIRONMENTS.md # Environment system details +``` + +## Future Enhancements + +Possible additions for future versions: + +1. **Cloud Provider Integration** + - AWS/GCP/Azure deployment options + - Cloud-specific SSL/TLS configuration + +2. **Advanced Validation** + - API key testing before saving + - Port availability checking + - Database connectivity testing + +3. **Migration Tools** + - Migrate from old config format + - Import/export environment configs + - Clone environment settings + +4. **Kubernetes Wizard** + - K8s cluster selection + - Namespace configuration + - Ingress setup + +5. **Service Discovery** + - Auto-detect running services + - Suggest optimal configurations + - Health check integration + +## Testing the Wizard + +### Quick Test (No Tailscale) +```bash +make wizard +# Answer prompts: +# - Secrets: Press Enter to generate JWT, provide fake API keys +# - Tailscale: N +# - Environment: dev, offset 0, no optional services +# Result: Should create .env.secrets and environments/dev.env +``` + +### Full Test (With Tailscale) +```bash +# Requires: Tailscale installed and running +make wizard +# Answer prompts: +# - Secrets: Real API keys +# - Tailscale: Y, option 1 (Tailscale Serve) +# - Environment: prod, offset 0, all optional services +# Result: Complete production setup +``` + +### Individual Component Tests +```bash +make setup-secrets # Test secrets configuration +make setup-tailscale # Test Tailscale setup (requires Tailscale) +make setup-environment # Test environment creation +make check-secrets # Test validation +``` + +## Summary + +The setup wizard provides a comprehensive, user-friendly configuration experience that: + +โœ… Reduces setup time from 30+ minutes to 5-10 minutes +โœ… Eliminates configuration errors with validation +โœ… Supports both simple and complex deployments +โœ… Integrates seamlessly with existing infrastructure +โœ… Provides clear documentation and help +โœ… Uses standard tooling (Makefile) +โœ… Maintains backward compatibility + +**One command to rule them all:** +```bash +make wizard +``` + +That's it! The wizard handles everything else. diff --git a/Docs/setup/advanced/CADDY_SETUP.md b/Docs/setup/advanced/CADDY_SETUP.md new file mode 100644 index 00000000..c13e3983 --- /dev/null +++ b/Docs/setup/advanced/CADDY_SETUP.md @@ -0,0 +1,328 @@ +# Caddy Reverse Proxy Setup + +Caddy provides a **shared reverse proxy** that serves multiple Friend-Lite environments from a single domain using path-based routing. + +## Architecture + +**One Caddy instance serves ALL environments:** +``` +https://hostname/ โ†’ Environment list landing page +https://hostname/dev/ โ†’ Dev environment (WebUI + Backend) +https://hostname/test/ โ†’ Test environment (WebUI + Backend) +https://hostname/prod/ โ†’ Prod environment (WebUI + Backend) +https://hostname/dev/mycelia/ โ†’ Dev Mycelia (if enabled) +https://hostname/test/mycelia/ โ†’ Test Mycelia (if enabled) +``` + +**Why this architecture?** +- Single HTTPS endpoint (ports 80/443) +- No port conflicts between environments +- Automatic SSL with Tailscale certificates +- Clean URLs with path-based routing +- Easy to add/remove environments + +## Quick Start + +### 1. Enable Caddy in Configuration + +Edit `config-docker.env`: +```bash +USE_CADDY_PROXY=true +TAILSCALE_HOSTNAME=your-hostname.ts.net +``` + +### 2. Start Caddy (Once) + +```bash +# Start the shared Caddy instance +make caddy-start +``` + +Caddy will automatically: +- Generate the Caddyfile from your environments +- Provision Tailscale certificates (if needed) +- Start serving on ports 80 and 443 + +### 3. Start Your Environments + +```bash +# Start as many environments as you want +./start-env.sh dev +./start-env.sh test +./start-env.sh prod +``` + +Each environment runs on unique ports (via `PORT_OFFSET`), and Caddy routes requests based on the URL path. + +### 4. Access Your Environments + +Open your browser to: +- `https://your-hostname.ts.net/` - See list of all environments +- `https://your-hostname.ts.net/dev/` - Access dev environment +- `https://your-hostname.ts.net/test/` - Access test environment + +## Caddy Management Commands + +```bash +# Start Caddy (once for all environments) +make caddy-start + +# Check if Caddy is running +make caddy-status + +# View Caddy logs +make caddy-logs + +# Restart Caddy (after config changes) +make caddy-restart + +# Stop Caddy +make caddy-stop + +# Regenerate Caddyfile (after adding/removing environments) +make caddy-regenerate +``` + +## How It Works + +### 1. Environment Startup + +When you run `./start-env.sh `: +1. Loads environment config from `environments/.env` +2. Generates Caddyfile with routes for all environments +3. Checks if Caddy is running +4. **Does NOT start Caddy** (you manage it separately) +5. Starts the environment services + +### 2. Caddyfile Generation + +The `scripts/generate-caddyfile.sh` script: +1. Scans `environments/` directory +2. Creates route for each environment: + ``` + //* โ†’ environment WebUI and Backend + //mycelia/* โ†’ Mycelia (if enabled) + ``` +3. Generates landing page with links to all environments + +### 3. Request Routing + +When a request comes in: +``` +https://hostname/dev/api/health + โ†“ + Caddy receives request + โ†“ + Matches route: /dev/* + โ†“ + Strips /dev/ prefix + โ†“ + Forwards to: friend-lite-dev-friend-backend-1:8000/api/health +``` + +## Directory Structure + +``` +friend-lite/ +โ”œโ”€โ”€ compose/ +โ”‚ โ””โ”€โ”€ caddy.yml # Shared Caddy compose file +โ”œโ”€โ”€ caddy/ +โ”‚ โ””โ”€โ”€ Caddyfile # Auto-generated routing config +โ”œโ”€โ”€ certs/ +โ”‚ โ”œโ”€โ”€ hostname.crt # Tailscale SSL certificate +โ”‚ โ””โ”€โ”€ hostname.key # Tailscale SSL key +โ”œโ”€โ”€ environments/ +โ”‚ โ”œโ”€โ”€ dev.env # Dev environment config +โ”‚ โ”œโ”€โ”€ test.env # Test environment config +โ”‚ โ””โ”€โ”€ prod.env # Prod environment config +โ””โ”€โ”€ scripts/ + โ””โ”€โ”€ generate-caddyfile.sh # Caddyfile generator +``` + +## Common Workflows + +### Adding a New Environment + +```bash +# 1. Create environment config +make setup-environment +# (Creates environments/feature-123.env) + +# 2. Start the environment +./start-env.sh feature-123 + +# 3. Regenerate Caddyfile and restart Caddy +make caddy-regenerate +make caddy-restart +``` + +The new environment is now accessible at `https://hostname/feature-123/` + +### Removing an Environment + +```bash +# 1. Stop the environment +make env-stop ENV=feature-123 + +# 2. Delete environment config +rm environments/feature-123.env + +# 3. Regenerate Caddyfile and restart Caddy +make caddy-regenerate +make caddy-restart +``` + +### Updating Caddy Configuration + +```bash +# 1. Make changes (add/remove environments, modify routes) +vim scripts/generate-caddyfile.sh + +# 2. Regenerate and restart +make caddy-regenerate +make caddy-restart +``` + +## Troubleshooting + +### Port Already Allocated + +**Error**: `Bind for 0.0.0.0:80 failed: port is already allocated` + +**Cause**: Multiple Caddy instances trying to bind to ports 80/443 + +**Solution**: +```bash +# Stop all Caddy containers +docker ps -a | grep caddy | awk '{print $1}' | xargs docker stop +docker ps -a | grep caddy | awk '{print $1}' | xargs docker rm + +# Start the shared Caddy instance +make caddy-start +``` + +### Caddy Not Routing Requests + +**Check Caddyfile**: +```bash +cat caddy/Caddyfile +``` + +**Regenerate if outdated**: +```bash +make caddy-regenerate +make caddy-restart +``` + +### Certificate Issues + +**Check certificate status**: +```bash +ls -la certs/ +openssl x509 -enddate -noout -in certs/your-hostname.crt +``` + +**Regenerate certificates**: +```bash +tailscale cert your-hostname.ts.net +mv your-hostname.* certs/ +make caddy-restart +``` + +### Environment Not Found + +**Error**: `404` when accessing `https://hostname/dev/` + +**Cause**: Environment not running or not in Caddyfile + +**Solution**: +```bash +# Check if environment is running +docker compose -p friend-lite-dev ps + +# Check if Caddyfile has the route +grep "/dev/" caddy/Caddyfile + +# If missing, regenerate +make caddy-regenerate +make caddy-restart +``` + +## Advanced Configuration + +### Custom Routes + +Edit `scripts/generate-caddyfile.sh` to add custom routes: + +```bash +# Add custom backend route +handle_path /${env_name}/custom/* { + reverse_proxy custom-service:8080 +} +``` + +### Multiple Domains + +To serve different environments on different domains: + +```caddyfile +# In generate-caddyfile.sh, create multiple server blocks +https://dev.example.com { + reverse_proxy friend-lite-dev-webui-1:80 +} + +https://prod.example.com { + reverse_proxy friend-lite-prod-webui-1:80 +} +``` + +### Load Balancing + +For high availability, use Caddy's load balancing: + +```caddyfile +reverse_proxy friend-lite-dev-backend-1:8000 friend-lite-dev-backend-2:8000 { + lb_policy round_robin + health_uri /health +} +``` + +## Migration from Per-Environment Caddy + +If you previously had Caddy running per-environment: + +```bash +# 1. Stop all environments +docker compose -p friend-lite-dev down +docker compose -p friend-lite-test down + +# 2. Remove old Caddy containers +docker ps -a | grep caddy | awk '{print $1}' | xargs docker rm + +# 3. Start shared Caddy +make caddy-start + +# 4. Restart environments (without Caddy) +./start-env.sh dev +./start-env.sh test +``` + +## Summary + +**Key Points:** +- โœ… One Caddy instance for all environments +- โœ… Caddy is managed separately from environments +- โœ… Use `make caddy-start` once, then start environments normally +- โœ… Path-based routing: `https://hostname//` +- โœ… Automatic certificate provisioning with Tailscale +- โœ… No port conflicts between environments + +**Quick Reference:** +```bash +make caddy-start # Start Caddy (once) +./start-env.sh dev # Start dev environment +./start-env.sh test # Start test environment +make caddy-status # Check if Caddy is running +make caddy-regenerate # Update routes after adding environments +make caddy-restart # Apply configuration changes +``` diff --git a/ENVIRONMENTS.md b/Docs/setup/advanced/ENVIRONMENTS.md similarity index 98% rename from ENVIRONMENTS.md rename to Docs/setup/advanced/ENVIRONMENTS.md index 469c5836..5aefcde1 100644 --- a/ENVIRONMENTS.md +++ b/Docs/setup/advanced/ENVIRONMENTS.md @@ -1,6 +1,6 @@ # Multi-Environment Management -Friend-Lite supports running multiple environments simultaneously with isolated databases and different ports. This is perfect for: +Chronicle supports running multiple environments simultaneously with isolated databases and different ports. This is perfect for: - **Git worktrees** - Work on multiple branches simultaneously - **Feature development** - Isolated testing environments - **Parallel testing** - Run tests while developing @@ -217,7 +217,7 @@ make env-clean ENV=quick-test ## Configuration Layers -Friend-Lite uses a layered configuration system for **Docker Compose** deployments: +Chronicle uses a layered configuration system for **Docker Compose** deployments: ### 1. `docker-defaults.env` - System Constants Infrastructure URLs and defaults (rarely changed): diff --git a/Docs/setup/advanced/MULTI_ENVIRONMENT_ARCHITECTURE.md b/Docs/setup/advanced/MULTI_ENVIRONMENT_ARCHITECTURE.md new file mode 100644 index 00000000..869df0d9 --- /dev/null +++ b/Docs/setup/advanced/MULTI_ENVIRONMENT_ARCHITECTURE.md @@ -0,0 +1,406 @@ +# Multi-Environment Architecture + +This document explains the Chronicle multi-environment architecture with shared services. + +## Overview + +Chronicle uses a **shared services** architecture that allows you to run multiple isolated environments simultaneously without port conflicts or resource duplication. + +### Shared Services (One Instance for All Environments) + +1. **Infrastructure** (MongoDB, Redis, Qdrant) +2. **Caddy Reverse Proxy** (HTTPS with path-based routing) + +### Per-Environment Services + +1. **Backend API** (unique port per environment) +2. **Web UI** (unique port per environment) +3. **Workers** (background jobs) +4. **Mycelia** (optional, unique ports) + +## Architecture Diagram + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Shared Services โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ MongoDB โ”‚ โ”‚ Redis โ”‚ โ”‚ Qdrant โ”‚ โ”‚ +โ”‚ โ”‚ :27017 โ”‚ โ”‚ :6379 โ”‚ โ”‚ :6034 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Caddy Reverse Proxy โ”‚ โ”‚ +โ”‚ โ”‚ :80 (HTTP) :443 (HTTPS) โ”‚ โ”‚ +โ”‚ โ”‚ Path-based routing to all environments โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Dev Environmentโ”‚ โ”‚Test Environmentโ”‚ โ”‚Prod Environment โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ Backend :8000 โ”‚ โ”‚ Backend :8010 โ”‚ โ”‚ Backend :8020 โ”‚ +โ”‚ WebUI :3010 โ”‚ โ”‚ WebUI :3020 โ”‚ โ”‚ WebUI :3030 โ”‚ +โ”‚ Workers โ”‚ โ”‚ Workers โ”‚ โ”‚ Workers โ”‚ +โ”‚ DB: dev โ”‚ โ”‚ DB: test โ”‚ โ”‚ DB: prod โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Data Isolation + +Even though infrastructure is shared, each environment is completely isolated: + +### MongoDB Isolation +- Each environment uses a unique database name +- `dev` โ†’ `friend-lite-dev` database +- `test` โ†’ `friend-lite-test` database +- `prod` โ†’ `friend-lite-prod` database + +### Redis Isolation +- Each environment can use a different Redis database number (0-15) +- Or use key prefixes: `env:dev:*`, `env:test:*`, `env:prod:*` + +### Qdrant Isolation +- Each environment uses a different data path or collection prefix +- Collections are scoped per environment + +## Quick Start Guide + +### 1. First-Time Setup + +```bash +# Create the shared network +docker network create chronicle-network + +# Start shared infrastructure (once) +make infra-start + +# Start shared Caddy (once, if using Caddy) +make caddy-start +``` + +### 2. Create and Start Environments + +```bash +# Create environments +make wizard # Create dev environment +make wizard # Create test environment + +# Start environments +./start-env.sh dev +./start-env.sh test + +# Both environments now running on unique ports! +``` + +### 3. Access Your Environments + +**Direct Access (via localhost ports):** +- Dev: http://localhost:3010 +- Test: http://localhost:3020 + +**Caddy Access (via shared HTTPS):** +- Dev: https://your-hostname.ts.net/dev/ +- Test: https://your-hostname.ts.net/test/ +- Environment list: https://your-hostname.ts.net/ + +## Management Commands + +### Infrastructure Management + +```bash +make infra-start # Start shared infrastructure +make infra-status # Check infrastructure status +make infra-logs # View infrastructure logs +make infra-restart # Restart infrastructure +make infra-stop # Stop infrastructure (affects all environments!) +make infra-clean # Delete ALL data (DANGER!) +``` + +### Caddy Management + +```bash +make caddy-start # Start shared Caddy +make caddy-status # Check Caddy status +make caddy-logs # View Caddy logs +make caddy-restart # Restart Caddy +make caddy-regenerate # Regenerate Caddyfile after adding environments +make caddy-stop # Stop Caddy +``` + +### Environment Management + +```bash +make env-list # List all environments +make env-start ENV=dev # Start specific environment +make env-stop ENV=dev # Stop specific environment +make env-status # Show status of all environments +make env-clean ENV=dev # Clean specific environment data + +# Or use the script directly +./start-env.sh dev # Start dev environment +./start-env.sh test # Start test environment +``` + +## Typical Workflows + +### Starting Everything from Scratch + +```bash +# 1. Start shared infrastructure +make infra-start + +# 2. Start shared Caddy (optional, for HTTPS) +make caddy-start + +# 3. Start your environments +./start-env.sh dev +./start-env.sh test +./start-env.sh prod +``` + +### Adding a New Environment + +```bash +# 1. Create environment config +make wizard +# (Configure: name=staging, PORT_OFFSET=30, etc.) + +# 2. Start the environment +./start-env.sh staging + +# 3. If using Caddy, regenerate routes +make caddy-regenerate +make caddy-restart +``` + +Now accessible at: +- Direct: http://localhost:3040 +- Caddy: https://hostname/staging/ + +### Removing an Environment + +```bash +# 1. Stop the environment +make env-stop ENV=staging + +# 2. Delete environment config +rm environments/staging.env + +# 3. If using Caddy, regenerate routes +make caddy-regenerate +make caddy-restart + +# 4. Optional: Clean environment data +make env-clean ENV=staging +``` + +### Daily Development + +```bash +# Check what's running +make env-status +make infra-status +make caddy-status + +# Start working +./start-env.sh dev + +# View logs +docker compose -p friend-lite-dev logs -f + +# Stop when done +make env-stop ENV=dev +``` + +## Port Allocation + +### Shared Services (Fixed Ports) + +| Service | Port | Description | +|---------|------|-------------| +| MongoDB | 27017 | Database (shared) | +| Redis | 6379 | Cache (shared) | +| Qdrant HTTP | 6034 | Vector DB HTTP (shared) | +| Qdrant gRPC | 6033 | Vector DB gRPC (shared) | +| Caddy HTTP | 80 | Reverse proxy HTTP (shared) | +| Caddy HTTPS | 443 | Reverse proxy HTTPS (shared) | + +### Environment-Specific Ports (Base + PORT_OFFSET) + +| Service | Port Calculation | Example (offset=0) | Example (offset=10) | +|---------|-----------------|-------------------|---------------------| +| Backend | 8000 + offset | 8000 | 8010 | +| WebUI | 3010 + offset | 3010 | 3020 | +| Mycelia Backend | 5100 + offset | 5100 | 5110 | +| Mycelia Frontend | 3003 + offset | 3003 | 3013 | + +## Troubleshooting + +### Infrastructure Not Running + +**Error**: "โŒ Shared infrastructure services are not running!" + +**Solution**: +```bash +make infra-start +``` + +### Port Conflict on Infrastructure + +**Error**: "Bind for 0.0.0.0:27017 failed: port is already allocated" + +**Cause**: Old per-environment infrastructure containers still running + +**Solution**: +```bash +# Stop all environments +docker compose -p friend-lite-dev down +docker compose -p friend-lite-test down + +# Remove old infrastructure containers +docker ps -a | grep -E "(mongo|redis|qdrant)" | grep -v "friend-lite-" | awk '{print $1}' | xargs docker rm + +# Start shared infrastructure +make infra-start + +# Restart environments +./start-env.sh dev +./start-env.sh test +``` + +### Caddy Port Conflict + +**Error**: "Bind for 0.0.0.0:80 failed: port is already allocated" + +**Cause**: Multiple Caddy instances or old Caddy containers + +**Solution**: +```bash +# Stop all Caddy containers except the shared one +docker ps -a | grep caddy | grep -v "friend-lite-caddy" | awk '{print $1}' | xargs docker rm + +# Or restart the shared Caddy +make caddy-restart +``` + +### Environment Can't Connect to Database + +**Symptoms**: Backend shows connection errors + +**Check**: +```bash +# Verify infrastructure is running +make infra-status + +# Check environment variables +docker compose -p friend-lite-dev exec friend-backend env | grep MONGODB_URI + +# Test connection +docker compose -p friend-lite-dev exec friend-backend bash -c "mongosh \$MONGODB_URI --eval 'db.stats()'" +``` + +### Data From Different Environments Mixed + +**Cause**: Environment using wrong database name + +**Check**: +```bash +# Check environment .env file +cat backends/advanced/.env.dev | grep MONGODB_DATABASE + +# Should show: MONGODB_DATABASE=friend-lite-dev +``` + +## Migration from Old Architecture + +If you previously had per-environment infrastructure: + +### Step 1: Stop All Environments + +```bash +# Stop all running environments +docker compose -p friend-lite-dev down +docker compose -p friend-lite-test down +docker compose -p friend-lite-prod down +``` + +### Step 2: Remove Old Infrastructure Containers + +```bash +# List all infrastructure containers +docker ps -a | grep -E "(mongo|redis|qdrant|caddy)" + +# Remove old per-environment infrastructure +docker ps -a | grep "friend-lite-.*-mongo" | awk '{print $1}' | xargs docker rm +docker ps -a | grep "friend-lite-.*-redis" | awk '{print $1}' | xargs docker rm +docker ps -a | grep "friend-lite-.*-qdrant" | awk '{print $1}' | xargs docker rm +docker ps -a | grep "friend-lite-.*-caddy" | awk '{print $1}' | xargs docker rm +``` + +### Step 3: Start Shared Services + +```bash +# Start shared infrastructure +make infra-start + +# Start shared Caddy (if using) +make caddy-start +``` + +### Step 4: Restart Environments + +```bash +./start-env.sh dev +./start-env.sh test +./start-env.sh prod +``` + +## Benefits of This Architecture + +### Resource Efficiency +- โœ… Only one MongoDB, Redis, Qdrant instance +- โœ… Reduced memory usage (shared infrastructure) +- โœ… Faster environment startup (no infrastructure initialization) + +### No Port Conflicts +- โœ… Infrastructure uses fixed ports (shared) +- โœ… Environments use offset ports (isolated) +- โœ… Can run unlimited environments simultaneously + +### Data Isolation +- โœ… Each environment has unique database names +- โœ… No risk of data cross-contamination +- โœ… Easy to backup/restore per-environment + +### Simplified Management +- โœ… Single Caddyfile for all environments +- โœ… Unified infrastructure management +- โœ… Consistent networking (chronicle-network) + +### Easy Development +- โœ… Start any number of feature branches +- โœ… Test multiple versions side-by-side +- โœ… Production-like setup on local machine + +## Summary + +**Key Concepts:** +- **Shared Infrastructure**: MongoDB, Redis, Qdrant (one instance, all environments) +- **Shared Caddy**: Reverse proxy with path-based routing (one instance, all environments) +- **Isolated Environments**: Unique ports, unique database names (complete isolation) +- **Simple Workflow**: `make infra-start` once, then `./start-env.sh ` for each environment + +**Commands to Remember:** +```bash +make infra-start # Start infrastructure (once) +make caddy-start # Start Caddy (once) +./start-env.sh dev # Start dev environment +./start-env.sh test # Start test environment +make env-status # Check what's running +``` + +**URLs:** +- Localhost: `http://localhost:` +- Caddy: `https://hostname//` diff --git a/Docs/setup/advanced/README.md b/Docs/setup/advanced/README.md new file mode 100644 index 00000000..e21af00d --- /dev/null +++ b/Docs/setup/advanced/README.md @@ -0,0 +1,64 @@ +# Advanced Setup Guides + +This directory contains advanced and optional setup guides for Chronicle. + +## Available Guides + +### Multi-Environment Setup + +- **[ENVIRONMENTS.md](ENVIRONMENTS.md)** - Multi-environment management + - Running multiple environments simultaneously + - Git worktree integration + - Isolated databases per environment + - Port offset configuration + +- **[MULTI_ENVIRONMENT_ARCHITECTURE.md](MULTI_ENVIRONMENT_ARCHITECTURE.md)** - Architecture deep dive + - Shared vs per-environment services + - Data isolation strategies + - Resource optimization + - Production deployment patterns + +### Network & Access + +- **[CADDY_SETUP.md](CADDY_SETUP.md)** - Caddy reverse proxy setup + - Multi-environment path-based routing + - Shared HTTPS endpoint + - Automatic SSL with Tailscale + +- **[SSL_SETUP.md](SSL_SETUP.md)** - SSL/TLS certificate configuration + - Self-signed certificates + - Let's Encrypt setup + - Certificate management + +- **[TAILSCALE-SERVE-GUIDE.md](TAILSCALE-SERVE-GUIDE.md)** - Tailscale serve detailed guide + - Advanced `tailscale serve` configuration + - Custom port mapping + - Multiple service setup + +- **[TAILSCALE_GUIDE.md](TAILSCALE_GUIDE.md)** - Technical Tailscale reference + - Hostname vs IP concepts + - Finding your Tailscale hostname + - Network configuration details + +## When to Use These Guides + +These guides are **optional** and intended for: + +- **Advanced users** who need specific features +- **Production deployments** requiring SSL/TLS +- **Multi-environment setups** with Caddy proxy +- **Power users** customizing Tailscale configuration + +## For Beginners + +If you're just getting started, you **don't need** these guides. Follow the main setup: + +1. Start with **[INSTALL.md](../../../INSTALL.md)** for your operating system +2. Follow the **setup wizard** (`make wizard`) +3. Complete **basic Tailscale setup** in [Docs/setup/tailscale.md](../tailscale.md) + +Come back to these advanced guides when you need specific features! + +--- + +**Need help?** See the main [Chronicle Documentation](../../../CLAUDE.md) diff --git a/Docs/setup/advanced/SSL_SETUP.md b/Docs/setup/advanced/SSL_SETUP.md new file mode 100644 index 00000000..d5ed3107 --- /dev/null +++ b/Docs/setup/advanced/SSL_SETUP.md @@ -0,0 +1,467 @@ +# SSL/TLS Setup Guide for Friend-Lite + +This guide explains how Friend-Lite handles SSL/TLS encryption for secure communication between services, with special focus on Tailscale integration. + +## Overview + +Friend-Lite uses SSL/TLS for: +1. **Browser โ†’ Backend/WebUI**: HTTPS for web interface and microphone access +2. **Mobile โ†’ Backend**: Secure API connections +3. **Service โ†’ Service**: Encrypted communication between distributed services (using Tailscale) + +## SSL Architecture + +### Development (Local) +``` +Browser โ†’ Caddy (HTTPS) โ†’ Backend (HTTP internal) + โ†’ WebUI (HTTP internal) +``` + +### Production (Distributed with Tailscale) +``` +Mobile โ†’ Backend (HTTPS via Tailscale) +Browser โ†’ Backend (HTTPS via Tailscale) +Speaker Service โ†’ Backend (HTTPS via Tailscale) +``` + +## Configuration Variables + +Friend-Lite now includes SSL configuration in both Docker Compose and Kubernetes configs: + +### Docker Compose (config-docker.env) +```bash +# SSL/TLS Configuration +HTTPS_ENABLED=false # Enable HTTPS mode +SSL_CERT_PATH=./ssl/server.crt # Path to SSL certificate +SSL_KEY_PATH=./ssl/server.key # Path to SSL private key +TAILSCALE_HOSTNAME= # Your Tailscale hostname (e.g., friend-lite.tailxxxxx.ts.net) +``` + +### Kubernetes (config-k8s.env) +```bash +# SSL/TLS Configuration +HTTPS_ENABLED=true # Always true for production +SSL_CERT_SECRET=friend-lite-tls # K8s secret containing TLS cert +TAILSCALE_HOSTNAME=friend-lite.tailxxxxx.ts.net +``` + +## Setup Methods + +### Method 1: Caddy for Development (Recommended for Local) + +**Best for**: Local development with browser microphone access + +**How it works**: Caddy automatically generates self-signed certificates for localhost + +**Setup**: +```bash +# 1. Enable Caddy profile in start-env.sh +./start-env.sh dev --profile https + +# 2. Access services +https://localhost/ # Web UI (accept self-signed cert warning) +wss://localhost/ws_pcm # WebSocket audio streaming +``` + +**Caddy automatically handles**: +- SSL certificate generation (self-signed for localhost) +- HTTPS โ†’ HTTP proxying to internal services +- WebSocket upgrade handling +- CORS for secure origins + +### Method 2: Self-Signed Certificates with Tailscale + +**Best for**: Distributed deployments, testing mobile apps, service-to-service communication + +**How it works**: Generate certificates for your Tailscale hostname, services communicate over Tailscale VPN + +**Setup**: + +1. **Get your Tailscale hostname**: + ```bash + tailscale status + # Look for: friend-lite 100.x.x.x yourname-friend-lite.tailxxxxx.ts.net + ``` + +2. **Generate SSL certificates**: + ```bash + # For advanced backend + cd backends/advanced + ./ssl/generate-ssl.sh friend-lite.tailxxxxx.ts.net + + # For speaker recognition + cd extras/speaker-recognition + ./ssl/generate-ssl.sh speaker.tailxxxxx.ts.net + ``` + +3. **Configure environment**: + Edit `config-docker.env`: + ```bash + HTTPS_ENABLED=true + SSL_CERT_PATH=./ssl/server.crt + SSL_KEY_PATH=./ssl/server.key + TAILSCALE_HOSTNAME=friend-lite.tailxxxxx.ts.net + ``` + +4. **Update CORS origins**: + The system automatically adds Tailscale hostname to CORS origins when `TAILSCALE_HOSTNAME` is set. + +5. **Start services**: + ```bash + ./start-env.sh dev + ``` + +6. **Access services**: + ``` + https://friend-lite.tailxxxxx.ts.net:8000/ # Backend API + https://friend-lite.tailxxxxx.ts.net:5173/ # Web UI + ``` + +### Method 3: Tailscale HTTPS (Production) + +**Best for**: Production deployments with automatic HTTPS + +**How it works**: Tailscale can serve HTTPS endpoints with automatic certificate management + +**Setup**: + +1. **Enable Tailscale HTTPS** on your machine: + ```bash + tailscale serve https / http://localhost:8000 + tailscale serve https / http://localhost:5173 + ``` + +2. **Configure environment**: + ```bash + HTTPS_ENABLED=true + TAILSCALE_HOSTNAME=friend-lite.tailxxxxx.ts.net + ``` + +3. **Access services**: + ``` + https://friend-lite.tailxxxxx.ts.net/ # Auto-managed HTTPS! + ``` + +Tailscale automatically: +- Generates valid TLS certificates +- Handles certificate renewal +- Provides secure DNS names +- No browser warnings! + +## Certificate Renewal + +### Tailscale Certificates (Caddy Path-Based Routing) + +**Certificate Lifecycle**: Tailscale certificates expire every **90 days**. + +**Automatic Checking**: The `start-env.sh` script automatically checks certificate expiry when starting environments: +```bash +./start-env.sh prod +# Output: +# โœ… Tailscale certificates found at: certs/orion.spangled-kettle.ts.net.crt +# Valid until: Mar 8 15:23:45 2025 GMT (87 days) +``` + +**Warning Indicators**: +- **< 30 days**: Warning shown with renewal reminder +- **Expired**: Error shown, environment won't start with HTTPS + +**Manual Renewal**: +```bash +# Check current expiry +openssl x509 -enddate -noout -in certs/your-hostname.ts.net.crt + +# Renew certificate +tailscale cert your-hostname.ts.net + +# Move new certificates to certs directory +mv your-hostname.ts.net.crt certs/ +mv your-hostname.ts.net.key certs/ + +# Restart Caddy to use new certificates +docker compose -p friend-lite-prod restart caddy +``` + +**Certificate Location**: `/Users/you/repos/friend-lite/certs/` + +**Caddy Mount**: Certificates are mounted read-only in Caddy container: +```yaml +volumes: + - ../../../certs:/certs:ro # Read-only access +``` + +**No Downtime Renewal**: +1. Obtain new certificate with `tailscale cert` +2. Replace files in `certs/` directory +3. Reload Caddy: `docker compose exec caddy caddy reload` + +**Production Best Practice**: Set a calendar reminder to renew certificates 2 weeks before expiry. + +### Self-Signed Certificates + +**Certificate Lifecycle**: Generated with 365-day validity by default. + +**Renewal**: +```bash +# Regenerate certificate +cd backends/advanced +./ssl/generate-ssl.sh your-hostname.ts.net + +# Restart services +docker compose restart +``` + +**Automated Checking**: Self-signed certificates are checked the same way as Tailscale certificates by `start-env.sh`. + +## Service-Specific SSL Setup + +### Advanced Backend + WebUI + +**Option A: Caddy (Development)** +```bash +cd backends/advanced +docker compose --profile https up --build -d +``` + +**Option B: Direct HTTPS (Production)** +1. Generate certificates: + ```bash + ./ssl/generate-ssl.sh friend-lite.tailxxxxx.ts.net + ``` + +2. Configure `.env`: + ```bash + HTTPS_ENABLED=true + SSL_CERT_PATH=./ssl/server.crt + SSL_KEY_PATH=./ssl/server.key + ``` + +### Speaker Recognition + +The speaker recognition service includes both backend and frontend: + +1. **Generate certificates**: + ```bash + cd extras/speaker-recognition + ./ssl/generate-ssl.sh speaker.tailxxxxx.ts.net + ``` + +2. **Configure environment**: + ```bash + HTTPS_ENABLED=true + SSL_CERT_PATH=./ssl/server.crt + SSL_KEY_PATH=./ssl/server.key + SPEAKER_BACKEND_URL=https://speaker.tailxxxxx.ts.net:8085 + ``` + +3. **Update Advanced Backend** to use HTTPS speaker service: + In `config-docker.env`: + ```bash + SPEAKER_SERVICE_URL=https://speaker.tailxxxxx.ts.net:8085 + ``` + +### Mobile App + +The mobile app needs to trust your SSL certificates: + +**For self-signed certificates**: +1. Install Tailscale on mobile device +2. Access services via Tailscale hostname +3. Accept certificate warnings (development only) + +**For production**: +Use Tailscale HTTPS (Method 3) - no warnings! + +## Integration with start-env.sh + +The `start-env.sh` script can automatically configure SSL based on environment: + +### Example: SSL-enabled environment + +Create `environments/prod.env`: +```bash +# Environment name +ENV_NAME=prod +PORT_OFFSET=0 + +# Database isolation +MONGODB_DATABASE=friend-lite-prod +MYCELIA_DB=mycelia-prod + +# SSL Configuration +HTTPS_ENABLED=true +TAILSCALE_HOSTNAME=friend-lite.tailxxxxx.ts.net +``` + +Start with SSL: +```bash +./start-env.sh prod +``` + +The script will: +1. Load SSL configuration from environment file +2. Export `HTTPS_ENABLED` and `TAILSCALE_HOSTNAME` +3. Update CORS origins to include Tailscale hostname +4. Generate backend `.env` with SSL settings + +## Certificate Generation Details + +The `ssl/generate-ssl.sh` script creates certificates with: + +**Subject Alternative Names (SANs)**: +- `localhost` +- `*.localhost` +- `127.0.0.1` +- Your Tailscale hostname or IP + +**Usage**: +```bash +# IP address +./ssl/generate-ssl.sh 100.83.66.30 + +# Domain name +./ssl/generate-ssl.sh friend-lite.tailxxxxx.ts.net + +# Dual (for Tailscale - both hostname and IP work) +./ssl/generate-ssl.sh friend-lite.tailxxxxx.ts.net +``` + +## CORS Configuration + +SSL affects CORS origins. The system automatically handles this: + +### Development (HTTP) +```bash +CORS_ORIGINS=http://localhost:5173,http://localhost:3000 +``` + +### Development (HTTPS via Caddy) +```bash +CORS_ORIGINS=https://localhost,https://localhost:5173 +``` + +### Production (HTTPS via Tailscale) +```bash +CORS_ORIGINS=https://friend-lite.tailxxxxx.ts.net,https://friend-lite.tailxxxxx.ts.net:8000,https://friend-lite.tailxxxxx.ts.net:5173 +``` + +The configuration system automatically updates CORS when `TAILSCALE_HOSTNAME` is set. + +## Troubleshooting + +### Issue: "Certificate not valid for hostname" + +**Cause**: Certificate SAN doesn't include your Tailscale hostname + +**Solution**: Regenerate certificate with correct hostname: +```bash +./ssl/generate-ssl.sh your-correct-hostname.tailxxxxx.ts.net +``` + +### Issue: Browser shows "NET::ERR_CERT_AUTHORITY_INVALID" + +**Cause**: Self-signed certificate not trusted by browser + +**Solutions**: +1. **Development**: Accept the warning (click "Advanced" โ†’ "Proceed") +2. **Production**: Use Tailscale HTTPS (automatic trust) +3. **Advanced**: Install certificate in system trust store + +### Issue: Mobile app can't connect + +**Cause**: Mobile OS doesn't trust self-signed certificate + +**Solutions**: +1. **Recommended**: Use Tailscale HTTPS (Method 3) +2. **Development**: Install Tailscale on mobile device, access via Tailscale network +3. **Testing only**: Disable SSL verification in app (NOT for production) + +### Issue: Service-to-service SSL errors + +**Cause**: Services can't verify each other's certificates + +**Solution**: Use Tailscale network - all services can communicate securely: +```bash +# In config-docker.env +SPEAKER_SERVICE_URL=https://speaker.tailxxxxx.ts.net:8085 +PARAKEET_ASR_URL=https://parakeet.tailxxxxx.ts.net:8767 +``` + +### Issue: WebSocket connections fail over HTTPS + +**Cause**: WebSocket upgrade headers not properly proxied + +**Solutions**: +1. **Caddy**: Already handles WebSocket upgrades automatically +2. **Direct HTTPS**: Ensure WebSocket endpoint uses `wss://` scheme +3. **Check CORS**: WebSocket origins must be in CORS_ORIGINS + +## Security Best Practices + +### Development +โœ… Self-signed certificates are fine +โœ… Caddy with automatic localhost certs +โœ… Accept browser warnings + +### Production +โœ… Use Tailscale HTTPS (automatic cert management) +โœ… Or: Use Let's Encrypt certificates (if publicly accessible) +โœ… Never commit certificates to git +โœ… Rotate certificates regularly (365 days for self-signed) +โœ… Use strong private keys (2048-bit RSA minimum) + +### Network Security +โœ… Use Tailscale for service-to-service communication +โœ… Restrict Tailscale ACLs to necessary services +โœ… Keep services on private networks (not public internet) +โœ… Use HTTPS for all external endpoints + +## Quick Reference + +| Use Case | Method | Setup Command | +|----------|--------|---------------| +| Local development | Caddy | `./start-env.sh dev --profile https` | +| Tailscale testing | Self-signed | `./ssl/generate-ssl.sh ` | +| Production | Tailscale HTTPS | `tailscale serve https / http://localhost:8000` | +| Distributed services | Tailscale VPN | Configure service URLs with Tailscale hostnames | + +## Advanced: Distributed Deployment + +For distributed deployments (e.g., backend on one machine, speaker service on another): + +1. **Install Tailscale on all machines**: + ```bash + curl -fsSL https://tailscale.com/install.sh | sh + sudo tailscale up + ``` + +2. **Generate certificates on each machine**: + ```bash + # Machine 1 (backend) + ./ssl/generate-ssl.sh backend.tailxxxxx.ts.net + + # Machine 2 (speaker) + ./ssl/generate-ssl.sh speaker.tailxxxxx.ts.net + ``` + +3. **Configure cross-service communication**: + ```bash + # On backend machine - config-docker.env + SPEAKER_SERVICE_URL=https://speaker.tailxxxxx.ts.net:8085 + + # On speaker machine - config-docker.env + BACKEND_URL=https://backend.tailxxxxx.ts.net:8000 + ``` + +4. **Services automatically discover each other via Tailscale**! + +## Summary + +SSL/TLS configuration in Friend-Lite is flexible and adapts to your deployment: + +- **Local development**: Caddy with automatic self-signed certs +- **Testing**: Self-signed certs for Tailscale hostnames +- **Production**: Tailscale HTTPS for automatic certificate management +- **Distributed**: Tailscale VPN for secure service-to-service communication + +The configuration system automatically handles CORS, certificate paths, and service URLs based on your settings in `config-docker.env` or `config-k8s.env`. diff --git a/Docs/setup/advanced/TAILSCALE-SERVE-GUIDE.md b/Docs/setup/advanced/TAILSCALE-SERVE-GUIDE.md new file mode 100644 index 00000000..5319daf9 --- /dev/null +++ b/Docs/setup/advanced/TAILSCALE-SERVE-GUIDE.md @@ -0,0 +1,215 @@ +# Tailscale Serve Setup Guide + +This guide explains how to use Tailscale serve with Friend-Lite for simple, single-environment deployments. + +## Overview + +**Tailscale Serve** is perfect when you want: +- โœ… One environment (e.g., just "production" or "serve") +- โœ… Simple setup with automatic HTTPS +- โœ… Minimal resource usage (no Caddy container) +- โœ… Quick remote access from any device + +**Use Caddy instead** if you need multiple environments (dev/test/prod) running simultaneously. + +## Quick Setup + +### Option 1: During Initial Setup (Wizard) + +```bash +make wizard +``` + +When prompted for HTTPS configuration, choose **Option 1: Use 'tailscale serve'**. + +The wizard will: +1. Detect your Tailscale hostname +2. Ask which environment to configure (default: `serve`) +3. Automatically configure all required routes +4. Display your service URL + +### Option 2: Manual Setup (Existing Installation) + +```bash +# Configure Tailscale serve for an environment +make configure-tailscale-serve + +# Or run the script directly with environment name +./scripts/configure-tailscale-serve.sh serve +``` + +## What Gets Configured + +Tailscale serve automatically sets up these routes: + +``` +https://your-hostname.ts.net/ +โ”œโ”€โ”€ / โ†’ Frontend (WebUI) +โ”œโ”€โ”€ /api โ†’ Backend API routes +โ”œโ”€โ”€ /auth โ†’ Authentication endpoints +โ”œโ”€โ”€ /users โ†’ User management endpoints +โ”œโ”€โ”€ /docs โ†’ API documentation +โ”œโ”€โ”€ /health โ†’ Health check endpoint +โ”œโ”€โ”€ /readiness โ†’ Readiness probe +โ””โ”€โ”€ /ws_pcm โ†’ WebSocket audio streaming +``` + +## Port Handling + +The script automatically detects ports based on your environment's `PORT_OFFSET`: + +**Example: `serve` environment with `PORT_OFFSET=10`** +- Backend: `8010` (8000 + 10) +- WebUI: `3020` (3010 + 10) + +**Default environment (no offset)** +- Backend: `8000` +- WebUI: `3010` + +## Checking Configuration + +```bash +# View current Tailscale serve status +tailscale serve status + +# Example output: +# https://orion.spangled-kettle.ts.net (tailnet only) +# |-- / proxy http://localhost:3020 +# |-- /api proxy http://localhost:8010/api +# |-- /auth proxy http://localhost:8010/auth +# ... +``` + +## Common Tasks + +### Reconfigure for Different Environment + +```bash +# Reconfigure for 'prod' environment +./scripts/configure-tailscale-serve.sh prod +``` + +### Stop Tailscale Serve + +```bash +tailscale serve off +``` + +### Start Services with Tailscale Serve + +```bash +# 1. Start your environment +./start-env.sh serve + +# 2. Configure Tailscale serve (if not already configured) +make configure-tailscale-serve + +# 3. Access from any device in your tailnet +# https://your-hostname.ts.net/ +``` + +## Troubleshooting + +### "Tailscale not found" + +Install Tailscale: +```bash +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up +``` + +### "Tailscale is not running" + +Start Tailscale: +```bash +sudo tailscale up +``` + +### 405 Method Not Allowed / Routes Not Working + +The root `.env` file may be interfering. Ensure you've removed it: +```bash +# Check if root .env exists +ls -la .env + +# If it exists, back it up and remove +mv .env .env.old-backup + +# Restart your environment +./start-env.sh serve +``` + +### Wrong Ports Being Used + +Check your environment configuration: +```bash +# View environment settings +cat environments/serve.env | grep PORT_OFFSET + +# Verify running services +docker ps --format "{{.Names}}: {{.Ports}}" | grep -E "(backend|webui)" +``` + +The ports should match: `8000 + PORT_OFFSET` for backend, `3010 + PORT_OFFSET` for webui. + +## Comparison: Tailscale Serve vs Caddy + +| Feature | Tailscale Serve | Caddy Reverse Proxy | +|---------|----------------|---------------------| +| **Setup** | โœ… Very simple | โš ๏ธ More complex | +| **Resource Usage** | โœ… Minimal | โš ๏ธ Extra container | +| **Multiple Envs** | โŒ One at a time | โœ… Simultaneous | +| **Path Routing** | โš ๏ธ Manual config | โœ… Automatic | +| **Production** | โš ๏ธ Basic | โœ… Production-grade | +| **Middleware** | โŒ None | โœ… Rate limiting, etc. | +| **Best For** | Personal/single env | Team/multiple envs | + +## Files Modified + +When you run the setup, these files are updated: + +1. **`config-docker.env`** + - `HTTPS_ENABLED=true` + - `TAILSCALE_HOSTNAME=your-hostname.ts.net` + +2. **Tailscale serve configuration** (persistent across reboots) + - All routes configured via `tailscale serve` commands + +## Security Notes + +- โœ… All traffic is encrypted (HTTPS via Tailscale) +- โœ… Only accessible within your Tailscale network (tailnet) +- โœ… Tailscale handles authentication and certificates +- โš ๏ธ Not exposed to public internet unless you use `tailscale funnel` + +## Next Steps + +After configuring Tailscale serve: + +1. **Test the connection** + ```bash + # From any device on your tailnet + curl https://your-hostname.ts.net/health + ``` + +2. **Access the web interface** + - Open: `https://your-hostname.ts.net/` + - Login with your credentials + +3. **Connect mobile devices** + - Install Tailscale on mobile device + - Join your tailnet + - Open: `https://your-hostname.ts.net/` + +## Getting Help + +- **View configuration**: `tailscale serve status` +- **View logs**: `docker compose -p friend-lite-serve logs -f` +- **Reconfigure**: `make configure-tailscale-serve` +- **Full setup guide**: See `TAILSCALE_GUIDE.md` for distributed deployments + +## Related Documentation + +- [Distributed Deployment Guide](docs/distributed-deployment.md) +- [Environment System](environments/README.md) +- [Wizard Setup](WIZARD.md) diff --git a/Docs/setup/advanced/TAILSCALE_GUIDE.md b/Docs/setup/advanced/TAILSCALE_GUIDE.md new file mode 100644 index 00000000..c10f99b2 --- /dev/null +++ b/Docs/setup/advanced/TAILSCALE_GUIDE.md @@ -0,0 +1,281 @@ +# Tailscale Configuration Guide + +## Understanding Tailscale Concepts + +### Tailscale IP vs Hostname + +When you run `tailscale status`, you'll see output like: + +``` +anubis 100.83.66.30 anubis.tail12345.ts.net linux - +kraken 100.84.22.15 kraken.tail12345.ts.net linux - +friend 100.85.10.42 friend.tail12345.ts.net darwin - +``` + +Each machine has: +1. **Machine Name** (left): Short name (e.g., `anubis`) +2. **IP Address** (middle): Tailscale IP (e.g., `100.83.66.30`) +3. **Hostname** (right): Full DNS name (e.g., `anubis.tail12345.ts.net`) + +### Which One to Use? + +**Use the HOSTNAME (ends in .ts.net)**, NOT the IP address. + +**Why hostname instead of IP?** +- โœ… Permanent - doesn't change +- โœ… Works with SSL certificates +- โœ… Human-readable +- โœ… DNS resolution built-in +- โœ… Works across Tailscale networks + +**IP addresses can change** when: +- You rejoin Tailscale +- Network configuration changes +- Tailscale updates + +## Finding Your Tailscale Hostname + +### Method 1: Using `tailscale status` + +```bash +tailscale status +``` + +Output: +``` +anubis 100.83.66.30 anubis.tail12345.ts.net linux - + ^^^^^^^^^^^^^^^^^^^^^^^^ + This is your hostname! +``` + +**Your hostname is in the third column, ending in `.ts.net`** + +### Method 2: Using `tailscale status --json` + +```bash +tailscale status --json | grep DNSName +``` + +Output: +```json +"DNSName":"anubis.tail12345.ts.net." +``` + +Remove the trailing dot: `anubis.tail12345.ts.net` + +### Method 3: Check Tailscale Admin Console + +1. Visit https://login.tailscale.com/admin/machines +2. Find your machine in the list +3. The hostname is shown under "DNS name" + +## When You're Asked for Tailscale Hostname + +### During `make setup-tailscale` + +**Question:** "Tailscale hostname [anubis.tail12345.ts.net]:" + +**What to enter:** The hostname for **THIS machine** (where you're running the wizard) + +**Why:** This is used to: +1. Generate SSL certificates for this machine +2. Configure CORS to allow connections from this hostname +3. Set up URLs for accessing this machine's services + +**Example:** +```bash +# You're on machine "anubis" +tailscale status shows: + anubis 100.83.66.30 anubis.tail12345.ts.net linux - + +Enter: anubis.tail12345.ts.net +``` + +### During `make setup-environment` + +**Question:** "Tailscale hostname (or press Enter to skip):" + +**What to enter:** Same hostname as before (usually auto-filled if you ran `make setup-tailscale`) + +**Why asked again:** In case you: +- Skipped Tailscale setup earlier +- Want to create an environment for a different machine +- Ran `make setup-environment` standalone + +**Difference from first question:** +- First question (setup-tailscale): Configures SSL certificates and validates Tailscale +- Second question (setup-environment): Just saves hostname to environment config +- **They should usually be the same!** + +## Common Scenarios + +### Scenario 1: Single Machine Setup + +You have one machine running Friend-Lite: + +```bash +# Run wizard on your machine +make wizard + +# Tailscale setup: +Tailscale hostname: friend-lite.tail12345.ts.net โ† Your machine's hostname + +# Environment setup: +Tailscale hostname: friend-lite.tail12345.ts.net โ† Same hostname +``` + +### Scenario 2: Distributed Setup (Backend + Speaker Service) + +You have two machines: +- Machine 1 (anubis): Backend +- Machine 2 (kraken): Speaker Recognition + +**On Machine 1 (Backend):** +```bash +make wizard +# Tailscale hostname: anubis.tail12345.ts.net โ† THIS machine's hostname + +# Then configure speaker service URL in config-docker.env: +SPEAKER_SERVICE_URL=https://kraken.tail12345.ts.net:8085 โ† OTHER machine +``` + +**On Machine 2 (Speaker Service):** +```bash +make wizard +# Tailscale hostname: kraken.tail12345.ts.net โ† THIS machine's hostname + +# Then configure backend URL in config-docker.env: +BACKEND_URL=https://anubis.tail12345.ts.net:8000 โ† OTHER machine +``` + +### Scenario 3: Multiple Environments on Same Machine + +You want dev, staging, and prod on the same machine: + +```bash +# Run wizard once +make wizard +# Tailscale hostname: friend-lite.tail12345.ts.net + +# All environments use the same hostname +./start-env.sh dev # Uses friend-lite.tail12345.ts.net:8000 +./start-env.sh staging # Uses friend-lite.tail12345.ts.net:8100 +./start-env.sh prod # Uses friend-lite.tail12345.ts.net:8200 +``` + +## Troubleshooting + +### Issue: "No hostname detected" + +**Cause:** Tailscale not running or `tailscale status --json` not available + +**Solution:** +```bash +# Check Tailscale status +tailscale status + +# If not running: +sudo tailscale up + +# Manually find hostname from status output +tailscale status +# Look at third column, copy the hostname ending in .ts.net +``` + +### Issue: "Should I use the IP or hostname?" + +**Answer:** Always use the **hostname** (ends in .ts.net) + +**Why:** +- IPs can change +- SSL certificates need hostnames +- DNS is more reliable +- Better for documentation + +### Issue: "I entered the IP address by mistake" + +**Solution:** Edit the environment file: +```bash +# Edit your environment file +nano environments/dev.env + +# Change: +TAILSCALE_HOSTNAME=100.83.66.30 # Wrong! IP address + +# To: +TAILSCALE_HOSTNAME=anubis.tail12345.ts.net # Correct! Hostname +``` + +### Issue: "Wizard shows different hostname than I expect" + +**Cause:** Auto-detection might pick the wrong machine + +**Solution:** Just enter the correct hostname manually: +```bash +# When prompted: +Tailscale hostname [wrong-hostname.ts.net]: +# Type: correct-hostname.ts.net +``` + +### Issue: "I'm not sure which machine I'm on" + +**Solution:** +```bash +# Check your machine name +hostname + +# Check Tailscale status +tailscale status +# The first line (with asterisk *) is THIS machine + +# Example output: +anubis* 100.83.66.30 anubis.tail12345.ts.net linux - + ^ + This asterisk means THIS is your current machine +``` + +## Quick Reference + +| Command | Purpose | +|---------|---------| +| `tailscale status` | Show all machines and their hostnames | +| `tailscale status --json` | JSON output for parsing | +| `hostname` | Show local machine name | +| `tailscale ip` | Show your Tailscale IP (don't use for wizard!) | + +| Concept | Example | Use in Wizard | +|---------|---------|---------------| +| Machine Name | `anubis` | No - too short | +| IP Address | `100.83.66.30` | No - can change | +| **Hostname** | `anubis.tail12345.ts.net` | **Yes - use this!** | + +## SSL Certificate Names + +When you generate SSL certificates, they'll include: +- `localhost` +- `127.0.0.1` +- Your Tailscale hostname (e.g., `anubis.tail12345.ts.net`) + +This means you can access services via: +- `https://localhost:8000` (on the machine itself) +- `https://anubis.tail12345.ts.net:8000` (from any Tailscale device) + +## Summary + +**Key Points:** +1. โœ… Use **hostname** (ends in `.ts.net`), NOT IP address +2. โœ… Find it with: `tailscale status` (third column) +3. โœ… Use the hostname for **THIS machine** (where you're running the wizard) +4. โœ… If wizard asks twice, enter the **same hostname** both times +5. โœ… For distributed setup, each machine uses its **own hostname** + +**Quick Check:** +```bash +# Am I using the right thing? +โœ… anubis.tail12345.ts.net # Correct - hostname +โŒ 100.83.66.30 # Wrong - IP address +โŒ anubis # Wrong - short name +โŒ tail12345.ts.net # Wrong - missing machine name +``` + +Still confused? Just run `tailscale status` and copy the **third column** for your machine! ๐ŸŽฏ diff --git a/INSTALL.md b/INSTALL.md index 7ebcccf5..d05dffff 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,7 +6,7 @@ Choose your operating system to get started with Chronicle: **Complete step-by-step guide for Windows users (including WSL2 setup)** -๐Ÿ‘‰ **[Windows Setup Guide](WINDOWS-SETUP.md)** +๐Ÿ‘‰ **[Windows Setup Guide](Docs/setup/windows-wsl2.md)** - Fresh Windows install instructions - Automated dependency installation @@ -224,9 +224,10 @@ Make sure Docker Desktop has WSL2 integration enabled: ## Advanced: Manual Dependency Installation If you prefer not to use the automated script, see: -- **Windows**: [WINDOWS-SETUP.md](WINDOWS-SETUP.md) - Step-by-step manual instructions -- **Linux**: [Docker Install Docs](https://docs.docker.com/engine/install/) -- **macOS**: [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) +- **Windows**: [Docs/setup/windows-wsl2.md](Docs/setup/windows-wsl2.md) - Step-by-step manual instructions +- **Windows (Git Bash)**: [Docs/setup/windows-gitbash.md](Docs/setup/windows-gitbash.md) - Alternative setup +- **Linux**: [Docs/setup/linux.md](Docs/setup/linux.md) - Linux manual setup +- **macOS**: [Docs/setup/macos.md](Docs/setup/macos.md) - macOS manual setup --- diff --git a/SETUP.md b/SETUP.md deleted file mode 100644 index 55125e11..00000000 --- a/SETUP.md +++ /dev/null @@ -1,218 +0,0 @@ -# Friend-Lite Setup Guide - -Quick setup guide for getting Friend-Lite running with Docker Compose. - -## Prerequisites - -- Docker and Docker Compose installed -- Git (if cloning from repository) - -## Initial Setup - -### 1. Set Up Secrets - -Copy the secrets template and add your credentials: - -```bash -cp .env.secrets.template .env.secrets -nano .env.secrets # or use your preferred editor -``` - -**Required secrets:** - -```bash -# Authentication -AUTH_SECRET_KEY=your-super-secret-jwt-key-change-this -ADMIN_EMAIL=admin@example.com -ADMIN_PASSWORD=your-secure-password - -# LLM (for memory extraction) -OPENAI_API_KEY=sk-your-openai-key-here - -# Transcription -DEEPGRAM_API_KEY=your-deepgram-key-here - -# Optional: Speaker Recognition -HF_TOKEN=hf_your_huggingface_token -``` - -**โš ๏ธ Important**: Never commit `.env.secrets` - it's gitignored for security. - -### 2. Review Configuration (Optional) - -Configuration is split into two files: - -**`config-docker.env`** - User settings (what you change): -```bash -# LLM provider (openai, ollama, groq) -LLM_PROVIDER=openai -OPENAI_MODEL=gpt-4o-mini - -# Transcription provider (deepgram, mistral, parakeet) -TRANSCRIPTION_PROVIDER=deepgram - -# Memory provider (friend_lite, openmemory_mcp) -MEMORY_PROVIDER=friend_lite -``` - -**`docker-defaults.env`** - System constants (rarely change): -- Infrastructure URLs (`mongodb://mongo:27017`) -- Service names and ports -- Only edit if using external services - -## Starting Friend-Lite - -### Single Environment (Default) - -Start the default development environment: - -```bash -./start-env.sh dev -``` - -Access at: -- **Web UI**: http://localhost:3010 -- **Backend API**: http://localhost:8000 -- **API Docs**: http://localhost:8000/docs - -### With Optional Services - -Start with Mycelia memory interface: - -```bash -./start-env.sh dev --profile mycelia -``` - -Start with speaker recognition: - -```bash -./start-env.sh dev --profile speaker -``` - -Combine multiple profiles: - -```bash -./start-env.sh dev --profile mycelia --profile speaker -``` - -### Multiple Environments Simultaneously - -See [ENVIRONMENTS.md](ENVIRONMENTS.md) for detailed multi-environment setup. - -Quick example: - -```bash -# Terminal 1: Dev environment -./start-env.sh dev - -# Terminal 2: Test environment (different ports/database) -./start-env.sh test -``` - -## Stopping Services - -Press `Ctrl+C` in the terminal running the services, or: - -```bash -make env-stop ENV=dev -``` - -## Verifying Installation - -### Check Services - -```bash -# Health check -curl http://localhost:8000/health - -# Readiness (checks all dependencies) -curl http://localhost:8000/readiness -``` - -### Check Logs - -```bash -# All services -docker compose logs - -# Specific service -docker compose logs friend-backend - -# Follow logs -docker compose logs -f friend-backend -``` - -### Login to Web UI - -1. Open http://localhost:3010 -2. Use credentials from `.env.secrets`: - - Email: `ADMIN_EMAIL` - - Password: `ADMIN_PASSWORD` - -## Troubleshooting - -### "env.secrets not found" Warning - -Create the secrets file: - -```bash -cp .env.secrets.template .env.secrets -nano .env.secrets # Add your credentials -``` - -### Port Conflicts - -If ports are already in use, edit `environments/dev.env`: - -```bash -PORT_OFFSET=1000 # Changes ports to 9000, 4010, etc. -``` - -### Service Won't Start - -Check logs: - -```bash -docker compose logs -``` - -Common issues: -- Missing secrets in `.env.secrets` -- Invalid API keys -- Insufficient Docker resources (increase memory limit) - -### Database Issues - -Reset the database (โš ๏ธ deletes all data): - -```bash -make env-clean ENV=dev -./start-env.sh dev -``` - -## Next Steps - -- **[ENVIRONMENTS.md](ENVIRONMENTS.md)** - Multi-environment management -- **[CLAUDE.md](CLAUDE.md)** - Complete project documentation -- **Backend Docs**: http://localhost:8000/docs (when running) -- **API Reference**: [docs/api-reference.md](docs/api-reference.md) - -## Configuration Files Reference - -| File | Purpose | You Edit? | Git? | -|------|---------|-----------|------| -| `.env.secrets` | **Your API keys and passwords** | โœ… Always | โŒ No | -| `.env.secrets.template` | Template for secrets | No | โœ… Yes | -| `config-docker.env` | **User settings** (providers, models) | โœ… Often | โœ… Yes | -| `docker-defaults.env` | System infrastructure URLs | Rarely | โœ… Yes | -| `config-k8s.env` | Kubernetes configuration | As needed | โœ… Yes | -| `config.env` | Config router (documentation) | No | โœ… Yes | -| `environments/dev.env` | Environment overrides | As needed | โœ… Yes | -| `docker-compose.yml` | Service definitions | No | โœ… Yes | - -## Support - -For issues and questions: -- Check logs: `docker compose logs ` -- Review [CLAUDE.md](CLAUDE.md) for detailed documentation -- Check [ENVIRONMENTS.md](ENVIRONMENTS.md) for environment setup diff --git a/backends/advanced/Docs/README.md b/backends/advanced/Docs/README.md index 50e91436..b486c3e5 100644 --- a/backends/advanced/Docs/README.md +++ b/backends/advanced/Docs/README.md @@ -38,12 +38,21 @@ Welcome to friend-lite! This guide provides the optimal reading sequence to unde - How conversations become memories - Mem0 integration and vector storage - Configuration and customization options -- **Code References**: +- **Code References**: - `src/advanced_omi_backend/memory/memory_service.py` (main processing) - `src/advanced_omi_backend/transcript_coordinator.py` (event coordination) - `src/advanced_omi_backend/conversation_repository.py` (data access) - `src/advanced_omi_backend/conversation_manager.py` (lifecycle management) +### 3b. **[Mycelia Setup Guide](./mycelia-setup.md)** ๐Ÿ”— *MEMORY INTERFACE* +**Complete guide for setting up Mycelia memory interface** +- What is Mycelia and when to use it +- Auto-login setup (recommended for web UI) +- OAuth credentials for API access +- Getting Client ID and token for frontend +- Troubleshooting authentication issues +- **Perfect for**: Users wanting advanced memory visualization and API access + ### 4. **[Authentication System](./auth.md)** **User management and security** - Dual authentication (email + user_id) @@ -105,6 +114,12 @@ Welcome to friend-lite! This guide provides the optimal reading sequence to unde 3. `src/advanced_omi_backend/users.py` - User management 4. `src/advanced_omi_backend/routers/api_router.py` - Auth router setup +### **"I want to set up Mycelia memory interface"** +1. [mycelia-setup.md](./mycelia-setup.md) - Complete setup guide +2. Set `MEMORY_PROVIDER=mycelia` in `.env` +3. Run `docker compose --profile mycelia up --build -d` +4. Check logs for OAuth credentials: `docker compose logs friend-backend | grep -A 10 "MYCELIA OAUTH"` + --- ## ๐Ÿ“‚ **File Organization Reference** diff --git a/chronicler.excalidraw b/chronicler.excalidraw new file mode 100644 index 00000000..028f19f3 --- /dev/null +++ b/chronicler.excalidraw @@ -0,0 +1,5646 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "50aqoS0EVA2qgHUbN07nt", + "type": "rectangle", + "x": 407.77928536016566, + "y": -120.9228704246267, + "width": 517.6205515600379, + "height": 225.16637158411416, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "iWUAVscXzoqyPit1gOg5R" + ], + "frameId": null, + "index": "Zu", + "roundness": { + "type": 3 + }, + "seed": 853723202, + "version": 375, + "versionNonce": 593469442, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "8rITVE7r-9pLidT2NSHJ3" + }, + { + "id": "wyDwwAPLPXarx3NIsaVRd", + "type": "arrow" + } + ], + "updated": 1760171118731, + "link": null, + "locked": false + }, + { + "id": "8rITVE7r-9pLidT2NSHJ3", + "type": "text", + "x": 552.5736004468253, + "y": -115.9228704246267, + "width": 228.03192138671875, + "height": 35, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "iWUAVscXzoqyPit1gOg5R" + ], + "frameId": null, + "index": "Zv", + "roundness": null, + "seed": 683546754, + "version": 265, + "versionNonce": 2079054110, + "isDeleted": false, + "boundElements": [], + "updated": 1760171118731, + "link": null, + "locked": false, + "text": "Deepgram worker", + "fontSize": 28, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": "50aqoS0EVA2qgHUbN07nt", + "originalText": "Deepgram worker", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "3WtKjlEwj_rAIEMF2zwR4", + "type": "rectangle", + "x": 418.8561350757905, + "y": -75.6826029445914, + "width": 186.96288896391286, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "iWUAVscXzoqyPit1gOg5R" + ], + "frameId": null, + "index": "Zw", + "roundness": { + "type": 3 + }, + "seed": 428860048, + "version": 1732, + "versionNonce": 1815105474, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "_itlNYZRAMJdi6TG5L46x" + }, + { + "id": "fL6YA-UBRXzcwAF3_FA5t", + "type": "arrow" + } + ], + "updated": 1760171118731, + "link": null, + "locked": false + }, + { + "id": "_itlNYZRAMJdi6TG5L46x", + "type": "text", + "x": 458.6211336348954, + "y": -70.63431298439934, + "width": 107.43289184570312, + "height": 64.9034200796159, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "iWUAVscXzoqyPit1gOg5R" + ], + "frameId": null, + "index": "Zx", + "roundness": null, + "seed": 9769104, + "version": 2269, + "versionNonce": 1770980702, + "isDeleted": false, + "boundElements": [], + "updated": 1760171118731, + "link": null, + "locked": false, + "text": "transcription\nconsumer\nDeepgram", + "fontSize": 17.307578687897575, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "3WtKjlEwj_rAIEMF2zwR4", + "originalText": "transcription consumer\nDeepgram", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "_5k_cIuYMliuU4rhfuCah", + "type": "rectangle", + "x": 517.8956186023447, + "y": 9.935948432274131, + "width": 190.82866510855592, + "height": 55, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "iWUAVscXzoqyPit1gOg5R" + ], + "frameId": null, + "index": "Zy", + "roundness": { + "type": 3 + }, + "seed": 965917918, + "version": 311, + "versionNonce": 2127890306, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "MOuM9ZZdsT2dX8f93VQk3" + } + ], + "updated": 1760171118731, + "link": null, + "locked": false + }, + { + "id": "MOuM9ZZdsT2dX8f93VQk3", + "type": "text", + "x": 542.6780084075992, + "y": 27.43594843227413, + "width": 141.26388549804688, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "iWUAVscXzoqyPit1gOg5R" + ], + "frameId": null, + "index": "Zz", + "roundness": null, + "seed": 943296770, + "version": 279, + "versionNonce": 1250077086, + "isDeleted": false, + "boundElements": [], + "updated": 1760171118731, + "link": null, + "locked": false, + "text": "Deepgram provider", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "_5k_cIuYMliuU4rhfuCah", + "originalText": "Deepgram provider", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "XcYntCXHXNpHOj61dCa-9", + "type": "arrow", + "x": -1351.165606641053, + "y": -53.02419261017326, + "width": 322.27862504143127, + "height": 5.6660519658897215, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": { + "type": 2 + }, + "seed": 1581161104, + "version": 385, + "versionNonce": 482930141, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "g1y2DqcVRsnwPGv9XAvX7" + } + ], + "updated": 1760118057898, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 322.27862504143127, + 5.6660519658897215 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "xnZp105apVNOn_w5xpohm", + "mode": "orbit", + "fixedPoint": [ + 0.06710258564529567, + 0.0671025856452953 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "g1y2DqcVRsnwPGv9XAvX7", + "type": "text", + "x": -892.8956656052067, + "y": -73.37109374230769, + "width": 80.87994384765625, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": null, + "seed": 783075472, + "version": 25, + "versionNonce": 649955472, + "isDeleted": false, + "boundElements": [], + "updated": 1758512667643, + "link": null, + "locked": false, + "text": "websocket\nJWT", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "XcYntCXHXNpHOj61dCa-9", + "originalText": "websocket\nJWT", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "bbh1IEJHD54RwLZr6YD6V", + "type": "rectangle", + "x": -1408.037990015313, + "y": -72.82045702011624, + "width": 60, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": { + "type": 3 + }, + "seed": 1831155344, + "version": 124, + "versionNonce": 1616198579, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Lh1SC9F6voEcdeMss8YaV" + }, + { + "id": "XN3k24gRDZgDVm_9pn-v6", + "type": "arrow" + }, + { + "id": "UcgqqSbbX8JwhXOWN7ZOE", + "type": "arrow" + } + ], + "updated": 1760118054013, + "link": null, + "locked": false + }, + { + "id": "Lh1SC9F6voEcdeMss8YaV", + "type": "text", + "x": -1398.7499727423638, + "y": -62.82045702011624, + "width": 41.42396545410156, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3", + "roundness": null, + "seed": 1583342736, + "version": 121, + "versionNonce": 1413041491, + "isDeleted": false, + "boundElements": [], + "updated": 1760118054013, + "link": null, + "locked": false, + "text": "client", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "bbh1IEJHD54RwLZr6YD6V", + "originalText": "client", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "xnZp105apVNOn_w5xpohm", + "type": "rectangle", + "x": -1028.1588675522944, + "y": -67.35814065966815, + "width": 100, + "height": 300, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4", + "roundness": { + "type": 3 + }, + "seed": 1880412816, + "version": 145, + "versionNonce": 671610974, + "isDeleted": false, + "boundElements": [ + { + "id": "svh_lJrG8tuYk2i_i77bJ", + "type": "text" + }, + { + "id": "XcYntCXHXNpHOj61dCa-9", + "type": "arrow" + }, + { + "id": "XN3k24gRDZgDVm_9pn-v6", + "type": "arrow" + }, + { + "id": "UcgqqSbbX8JwhXOWN7ZOE", + "type": "arrow" + }, + { + "id": "xJCudECerB0TZyUuc45lR", + "type": "arrow" + }, + { + "id": "ZNvdrbotyI5E9mP_7UKg8", + "type": "arrow" + }, + { + "id": "9oBCVc6ESpSpM3q6gAStp", + "type": "arrow" + }, + { + "id": "GO5h3hI0Wsmx1sviPag__", + "type": "arrow" + } + ], + "updated": 1760173773877, + "link": null, + "locked": false + }, + { + "id": "svh_lJrG8tuYk2i_i77bJ", + "type": "text", + "x": -1018.5988394761225, + "y": 42.64185934033185, + "width": 80.87994384765625, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a5", + "roundness": null, + "seed": 419168400, + "version": 154, + "versionNonce": 1028267293, + "isDeleted": false, + "boundElements": [], + "updated": 1760118079762, + "link": null, + "locked": false, + "text": "/ws_omi\n\nwebsocket\ncontroller", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "xnZp105apVNOn_w5xpohm", + "originalText": "/ws_omi\n\nwebsocket controller", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "3dEgTiQHodnwCm9C0Vd6x", + "type": "text", + "x": -876.7076340555377, + "y": -503.39180826326884, + "width": 545.5675659179688, + "height": 80, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a6", + "roundness": null, + "seed": 952987280, + "version": 326, + "versionNonce": 2047954754, + "isDeleted": false, + "boundElements": [], + "updated": 1760169765913, + "link": null, + "locked": false, + "text": "client id should be random\nwe shouldnt depend on it for anything\nremove suffixprefix and make \nClientType an Enum or something if we need to distinguish client type", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "client id should be random\nwe shouldnt depend on it for anything\nremove suffixprefix and make \nClientType an Enum or something if we need to distinguish client type", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "ZNvdrbotyI5E9mP_7UKg8", + "type": "arrow", + "x": -988.6294689774983, + "y": -69.46795330574105, + "width": 4.088392231852481, + "height": 126.70061149750632, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aB", + "roundness": { + "type": 2 + }, + "seed": 1539724432, + "version": 491, + "versionNonce": 758838274, + "isDeleted": false, + "boundElements": [], + "updated": 1760169762127, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 4.088392231852481, + -126.70061149750632 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "xnZp105apVNOn_w5xpohm", + "mode": "orbit", + "fixedPoint": [ + 0.35978446889218163, + 0.3597844688921793 + ] + }, + "endBinding": { + "elementId": "cjv9stICG5r3QDgmKpwMu", + "mode": "orbit", + "fixedPoint": [ + 0.5705033678497955, + 0.5705033678497959 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "sCu0zy9Jwva4rYmDo9KTy", + "type": "arrow", + "x": -987.336987853961, + "y": -268.16856480324736, + "width": 1.2010246983066963, + "height": 93.96514372587768, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aC", + "roundness": { + "type": 2 + }, + "seed": 1462583952, + "version": 832, + "versionNonce": 320352962, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "9Uio7c78T9lLIcmaUdGbD" + } + ], + "updated": 1760169762127, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1.2010246983066963, + -93.96514372587768 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "cjv9stICG5r3QDgmKpwMu", + "mode": "orbit", + "fixedPoint": [ + 0.5367924262991142, + 0.46320757370088433 + ] + }, + "endBinding": { + "elementId": "pWlG0R4sEnmmkq9NdWpav", + "mode": "orbit", + "fixedPoint": [ + 0.568706709066292, + 0.5687067090662936 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "9Uio7c78T9lLIcmaUdGbD", + "type": "text", + "x": -924.0953565014235, + "y": -333.5083469590229, + "width": 60.36793518066406, + "height": 20, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aD", + "roundness": null, + "seed": 1669755024, + "version": 15, + "versionNonce": 782409154, + "isDeleted": false, + "boundElements": [], + "updated": 1760169745877, + "link": null, + "locked": false, + "text": "register", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "sCu0zy9Jwva4rYmDo9KTy", + "originalText": "register", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "cjv9stICG5r3QDgmKpwMu", + "type": "rectangle", + "x": -1040.5890115741943, + "y": -267.16856480324736, + "width": 100, + "height": 70, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0UvLnVKhHpniA3ZzJH8zx" + ], + "frameId": null, + "index": "aDG", + "roundness": { + "type": 3 + }, + "seed": 2017533072, + "version": 347, + "versionNonce": 901688450, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "lUs_DvXgR5fie-sMPshhd" + }, + { + "id": "ZNvdrbotyI5E9mP_7UKg8", + "type": "arrow" + }, + { + "id": "VqjzSU51B4Ojjm3hGYxii", + "type": "arrow" + }, + { + "id": "sCu0zy9Jwva4rYmDo9KTy", + "type": "arrow" + } + ], + "updated": 1760169762127, + "link": null, + "locked": false + }, + { + "id": "lUs_DvXgR5fie-sMPshhd", + "type": "text", + "x": -1020.2209848407958, + "y": -262.16856480324736, + "width": 59.263946533203125, + "height": 60, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0UvLnVKhHpniA3ZzJH8zx" + ], + "frameId": null, + "index": "aDV", + "roundness": null, + "seed": 688851600, + "version": 378, + "versionNonce": 1744424002, + "isDeleted": false, + "boundElements": [], + "updated": 1760169762127, + "link": null, + "locked": false, + "text": "client\nstate\ncreated", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "cjv9stICG5r3QDgmKpwMu", + "originalText": "client state created", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "pWlG0R4sEnmmkq9NdWpav", + "type": "rectangle", + "x": -1068.4576104442347, + "y": -414.09181643912297, + "width": 140, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0UvLnVKhHpniA3ZzJH8zx" + ], + "frameId": null, + "index": "aDl", + "roundness": { + "type": 3 + }, + "seed": 636434576, + "version": 411, + "versionNonce": 129538882, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "jhXKpaJDe-WSRqw6Tnzmu" + }, + { + "id": "sCu0zy9Jwva4rYmDo9KTy", + "type": "arrow" + } + ], + "updated": 1760169762127, + "link": null, + "locked": false + }, + { + "id": "jhXKpaJDe-WSRqw6Tnzmu", + "type": "text", + "x": -1051.753569916891, + "y": -399.09181643912297, + "width": 106.5919189453125, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0UvLnVKhHpniA3ZzJH8zx" + ], + "frameId": null, + "index": "aE", + "roundness": null, + "seed": 848930448, + "version": 415, + "versionNonce": 709354242, + "isDeleted": false, + "boundElements": [], + "updated": 1760169762127, + "link": null, + "locked": false, + "text": "ClientManager", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "pWlG0R4sEnmmkq9NdWpav", + "originalText": "ClientManager", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "cQy9JkynAibsDJEnIBO16", + "type": "rectangle", + "x": -881.5334848377413, + "y": -412.3546888281358, + "width": 100, + "height": 60, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0UvLnVKhHpniA3ZzJH8zx" + ], + "frameId": null, + "index": "aEV", + "roundness": { + "type": 3 + }, + "seed": 680124048, + "version": 398, + "versionNonce": 1690864258, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "FuoCqzpV_SOJm4bgPjanE" + }, + { + "id": "VqjzSU51B4Ojjm3hGYxii", + "type": "arrow" + } + ], + "updated": 1760169762128, + "link": null, + "locked": false + }, + { + "id": "FuoCqzpV_SOJm4bgPjanE", + "type": "text", + "x": -876.4374383899874, + "y": -392.3546888281358, + "width": 89.80790710449219, + "height": 20, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0UvLnVKhHpniA3ZzJH8zx" + ], + "frameId": null, + "index": "aF", + "roundness": null, + "seed": 384797840, + "version": 452, + "versionNonce": 370666050, + "isDeleted": false, + "boundElements": [], + "updated": 1760169762128, + "link": null, + "locked": false, + "text": "users model", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "cQy9JkynAibsDJEnIBO16", + "originalText": "users model", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "VqjzSU51B4Ojjm3hGYxii", + "type": "arrow", + "x": -944.3776432768749, + "y": -263.619933304463, + "width": 117.51466708602527, + "height": 82.5744505519628, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aK", + "roundness": { + "type": 2 + }, + "seed": 1255518864, + "version": 789, + "versionNonce": 1843745282, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "LxJNFc_prHRuz7sbwHtD_" + } + ], + "updated": 1760169762128, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 117.51466708602527, + -82.5744505519628 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "cjv9stICG5r3QDgmKpwMu", + "mode": "orbit", + "fixedPoint": [ + 0.5072728154235108, + 0.5072728154235112 + ] + }, + "endBinding": { + "elementId": "cQy9JkynAibsDJEnIBO16", + "mode": "orbit", + "fixedPoint": [ + 0.8027784464638887, + 0.8027784464638889 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "LxJNFc_prHRuz7sbwHtD_", + "type": "text", + "x": -402.6040141228332, + "y": -123.19174869653406, + "width": 60.36793518066406, + "height": 20, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aL", + "roundness": null, + "seed": 681631888, + "version": 19, + "versionNonce": 423666832, + "isDeleted": false, + "boundElements": [], + "updated": 1758512667645, + "link": null, + "locked": false, + "text": "register", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "VqjzSU51B4Ojjm3hGYxii", + "originalText": "register", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "XN3k24gRDZgDVm_9pn-v6", + "type": "arrow", + "x": -1388.037990015313, + "y": -27.820457020116237, + "width": 354.8791224630186, + "height": 110.36231636044806, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aO", + "roundness": null, + "seed": 744483472, + "version": 371, + "versionNonce": 15346419, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "_bCPsAWItqbq6k8jWfH3W" + } + ], + "updated": 1760118054014, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 110.36231636044806 + ], + [ + 354.8791224630186, + 110.36231636044806 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "bbh1IEJHD54RwLZr6YD6V", + "focus": 0.7435897435897355, + "gap": 1, + "fixedPoint": [ + 0.3333333333333333, + 1.125 + ], + "mode": "orbit" + }, + "endBinding": { + "elementId": "xnZp105apVNOn_w5xpohm", + "focus": -0.06341463414634335, + "gap": 1, + "fixedPoint": [ + -0.05, + 0.4996666666666666 + ], + "mode": "orbit" + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "_bCPsAWItqbq6k8jWfH3W", + "type": "text", + "x": -1037.2995986938477, + "y": 66.52890624999998, + "width": 90.41592407226562, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aP", + "roundness": null, + "seed": 1277434000, + "version": 33, + "versionNonce": 1120965744, + "isDeleted": false, + "boundElements": [], + "updated": 1758512667643, + "link": null, + "locked": false, + "text": "audio chunk", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "XN3k24gRDZgDVm_9pn-v6", + "originalText": "audio chunk", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "UcgqqSbbX8JwhXOWN7ZOE", + "type": "arrow", + "x": -1368.037990015313, + "y": -27.820457020116237, + "width": 334.8791224630186, + "height": 60.462316360448085, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aQ", + "roundness": null, + "seed": 2134017680, + "version": 265, + "versionNonce": 1257133203, + "isDeleted": false, + "boundElements": [], + "updated": 1760118054014, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 60.462316360448085 + ], + [ + 334.8791224630186, + 60.462316360448085 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "bbh1IEJHD54RwLZr6YD6V", + "focus": -0.3333333333333336, + "gap": 5, + "fixedPoint": [ + 0.6666666666666666, + 1.125 + ], + "mode": "orbit" + }, + "endBinding": { + "elementId": "xnZp105apVNOn_w5xpohm", + "focus": 0.33333333333333304, + "gap": 5, + "fixedPoint": [ + -0.05, + 0.3333333333333333 + ], + "mode": "orbit" + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "fixedSegments": null, + "startIsSpecial": null, + "endIsSpecial": null + }, + { + "id": "q8qrTMvr_uPhiyP2Hrztr", + "type": "text", + "x": -1208.1588675522944, + "y": 12.641859340331848, + "width": 134.94386291503906, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aR", + "roundness": null, + "seed": 1648571536, + "version": 84, + "versionNonce": 1080253981, + "isDeleted": false, + "boundElements": [], + "updated": 1760117811214, + "link": null, + "locked": false, + "text": "control messages\naudio start/stop", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "control messages\naudio start/stop", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "xJCudECerB0TZyUuc45lR", + "type": "arrow", + "x": -927.1588675522944, + "y": -28.161692152607756, + "width": 277.4951340073303, + "height": 43.959262535146635, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ac", + "roundness": { + "type": 2 + }, + "seed": 723463824, + "version": 2557, + "versionNonce": 36607902, + "isDeleted": false, + "boundElements": [], + "updated": 1760169990132, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 277.4951340073303, + 43.959262535146635 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "xnZp105apVNOn_w5xpohm", + "mode": "orbit", + "fixedPoint": [ + 0.8763999039914677, + 0.1236000960085346 + ] + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "uODPRjUc9mhEWDAW3PHR2", + "type": "rectangle", + "x": -46.981821481774205, + "y": -570.066398840404, + "width": 240, + "height": 100, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ah", + "roundness": { + "type": 3 + }, + "seed": 1660949648, + "version": 413, + "versionNonce": 1281410526, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "hrgx703XXDXGttcQjUKxF" + }, + { + "id": "04xIJKT9TRHJvVeTLZiQU", + "type": "arrow" + } + ], + "updated": 1760174128093, + "link": null, + "locked": false + }, + { + "id": "hrgx703XXDXGttcQjUKxF", + "type": "text", + "x": 23.518178518225795, + "y": -532.566398840404, + "width": 99, + "height": 25, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ai", + "roundness": null, + "seed": 1362035344, + "version": 437, + "versionNonce": 1581799966, + "isDeleted": false, + "boundElements": [], + "updated": 1760174128093, + "link": null, + "locked": false, + "text": "AudioFile", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "uODPRjUc9mhEWDAW3PHR2", + "originalText": "AudioFile", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "04xIJKT9TRHJvVeTLZiQU", + "type": "arrow", + "x": -389.2555241807287, + "y": -325.3279116442713, + "width": 421.2765808865099, + "height": 137.9822709330794, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aj", + "roundness": { + "type": 2 + }, + "seed": 928002192, + "version": 1016, + "versionNonce": 272672862, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "u7wvC7BzsmrwHZLutQxNm" + } + ], + "updated": 1760174145629, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 421.2765808865099, + -137.9822709330794 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "qw4FSQEULfB3EwHremgmE", + "mode": "orbit", + "fixedPoint": [ + 0.5962389702150768, + 0.403761029784922 + ] + }, + "endBinding": { + "elementId": "uODPRjUc9mhEWDAW3PHR2", + "mode": "orbit", + "fixedPoint": [ + 0.7425885593537042, + 0.7425885593537049 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "u7wvC7BzsmrwHZLutQxNm", + "type": "text", + "x": -196.21720932341123, + "y": -404.319047110811, + "width": 35.199951171875, + "height": 20, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ak", + "roundness": null, + "seed": 1562091152, + "version": 83, + "versionNonce": 685104158, + "isDeleted": false, + "boundElements": [], + "updated": 1760174135275, + "link": null, + "locked": false, + "text": "add ", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "04xIJKT9TRHJvVeTLZiQU", + "originalText": "add ", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "qEsBJXWdxXEOgyZhG6jy3", + "type": "text", + "x": 183.43355460360817, + "y": -850.5306118511771, + "width": 510.3992919921875, + "height": 340, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "al", + "roundness": null, + "seed": 1397312656, + "version": 181, + "versionNonce": 1631785282, + "isDeleted": false, + "boundElements": [], + "updated": 1760174373839, + "link": null, + "locked": false, + "text": " \"_id\": ObjectId(\"507f1f77bcf86cd799439011\"),\n \"audio_uuid\": \"d8014de5-258e-4465-ac47-38eedbb996c2\",\n \"audio_path\": \"1708901234_user-device_d8014de5.wav\",\n \"client_id\": \"user-device\",\n \"user_id\": \"507f191e810c19729de860ea\",\n \"user_email\": \"user@example.com\",\n \"timestamp\": 1708901234000,\n \"cropped_audio_path\": null,\n \"conversation_id\": \"conv-uuid-5678\",\n \"has_speech\": true,\n \"speech_analysis\": {\n \"has_speech\": true,\n \"word_count\": 247,\n \"speech_start\": 2.3,\n \"speech_end\": 45.8,\n \"duration\": 43.5,\n \"reason\": \"Valid speech detected (247 words, 43.5s)\"", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": " \"_id\": ObjectId(\"507f1f77bcf86cd799439011\"),\n \"audio_uuid\": \"d8014de5-258e-4465-ac47-38eedbb996c2\",\n \"audio_path\": \"1708901234_user-device_d8014de5.wav\",\n \"client_id\": \"user-device\",\n \"user_id\": \"507f191e810c19729de860ea\",\n \"user_email\": \"user@example.com\",\n \"timestamp\": 1708901234000,\n \"cropped_audio_path\": null,\n \"conversation_id\": \"conv-uuid-5678\",\n \"has_speech\": true,\n \"speech_analysis\": {\n \"has_speech\": true,\n \"word_count\": 247,\n \"speech_start\": 2.3,\n \"speech_end\": 45.8,\n \"duration\": 43.5,\n \"reason\": \"Valid speech detected (247 words, 43.5s)\"", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "TX-TmXSnZlIg4_6oVMnk4", + "type": "text", + "x": 365.8082990406954, + "y": 120.72713661529428, + "width": 527.999267578125, + "height": 320, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0G", + "roundness": null, + "seed": 447569552, + "version": 488, + "versionNonce": 1565783490, + "isDeleted": false, + "boundElements": [], + "updated": 1760174057564, + "link": null, + "locked": false, + "text": " conversation_id: \"uuid\",\n audio_uuid: \"audio_uuid\",\n user_id: ObjectId,\n client_id: \"client_id\",\n title: \"uses aggregator so far\", # โ† Placeholder\n summary: \"uses aggregator so far\", # โ† Placeholder\n\n // Versioned system \n transcript_versions: [v1],\n active_transcript_version: v1,\n memory_versions: [],\n active_memory_version: null,\n\n // content\n transcript: \"full text\"\n segments: [array of SpeakerSegements]", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": " conversation_id: \"uuid\",\n audio_uuid: \"audio_uuid\",\n user_id: ObjectId,\n client_id: \"client_id\",\n title: \"uses aggregator so far\", # โ† Placeholder\n summary: \"uses aggregator so far\", # โ† Placeholder\n\n // Versioned system \n transcript_versions: [v1],\n active_transcript_version: v1,\n memory_versions: [],\n active_memory_version: null,\n\n // content\n transcript: \"full text\"\n segments: [array of SpeakerSegements]", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "WjJhcq4N1a2nBEde3yVSl", + "type": "rectangle", + "x": -1416.0175416205789, + "y": 338.33344608811933, + "width": 420, + "height": 240, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "DH2SSf3Ecn3ZmX9IDc8UI" + ], + "frameId": null, + "index": "b0IG", + "roundness": { + "type": 3 + }, + "seed": 177547408, + "version": 444, + "versionNonce": 1648911582, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "kCvHcQyYNc1KT8uGeBhbZ" + }, + { + "id": "NuXQqOfu6Jj2NoAWRoRx2", + "type": "arrow" + } + ], + "updated": 1760173836345, + "link": null, + "locked": false + }, + { + "id": "kCvHcQyYNc1KT8uGeBhbZ", + "type": "text", + "x": -1276.6894776557351, + "y": 343.33344608811933, + "width": 141.3438720703125, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "DH2SSf3Ecn3ZmX9IDc8UI" + ], + "frameId": null, + "index": "b0IV", + "roundness": null, + "seed": 1828820624, + "version": 469, + "versionNonce": 870586654, + "isDeleted": false, + "boundElements": [], + "updated": 1760173836345, + "link": null, + "locked": false, + "text": "speaker recognition", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "top", + "containerId": "WjJhcq4N1a2nBEde3yVSl", + "originalText": "speaker recognition", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "AoB4eB1ji5QbCsYepi9ft", + "type": "rectangle", + "x": -1381.5073284838388, + "y": 405.261031165565, + "width": 360, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "DH2SSf3Ecn3ZmX9IDc8UI" + ], + "frameId": null, + "index": "b0Il", + "roundness": { + "type": 3 + }, + "seed": 1672496272, + "version": 347, + "versionNonce": 292219294, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "1Mz8BphslqLmR4pFDTc4-" + } + ], + "updated": 1760173836345, + "link": null, + "locked": false + }, + { + "id": "1Mz8BphslqLmR4pFDTc4-", + "type": "text", + "x": -1353.2271771166513, + "y": 420.261031165565, + "width": 303.439697265625, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "DH2SSf3Ecn3ZmX9IDc8UI" + ], + "frameId": null, + "index": "b0J", + "roundness": null, + "seed": 744043152, + "version": 405, + "versionNonce": 232261086, + "isDeleted": false, + "boundElements": [], + "updated": 1760173836345, + "link": null, + "locked": false, + "text": "/diarize-identify-match { audio, transcript }", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "AoB4eB1ji5QbCsYepi9ft", + "originalText": "/diarize-identify-match { audio, transcript }", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "pS71mVHv_7YSFp4K6aFvJ", + "type": "rectangle", + "x": -1381.771734545008, + "y": 485.52095576807017, + "width": 300, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "DH2SSf3Ecn3ZmX9IDc8UI" + ], + "frameId": null, + "index": "b0JV", + "roundness": { + "type": 3 + }, + "seed": 229925008, + "version": 348, + "versionNonce": 1859623454, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Wam09WdlxApV_4acMtn94" + } + ], + "updated": 1760173836345, + "link": null, + "locked": false + }, + { + "id": "Wam09WdlxApV_4acMtn94", + "type": "text", + "x": -1334.2116301748908, + "y": 500.52095576807017, + "width": 204.87979125976562, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "DH2SSf3Ecn3ZmX9IDc8UI" + ], + "frameId": null, + "index": "b0K", + "roundness": null, + "seed": 930729616, + "version": 406, + "versionNonce": 1801102942, + "isDeleted": false, + "boundElements": [], + "updated": 1760173836345, + "link": null, + "locked": false, + "text": "/identify { audio, segments } ", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "pS71mVHv_7YSFp4K6aFvJ", + "originalText": "/identify { audio, segments } ", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "bYEREuhUrR7Oa81xDvjOG", + "type": "rectangle", + "x": -652.5212953369207, + "y": -11.053635414987525, + "width": 205.38199387938698, + "height": 55.57838705903249, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0j", + "roundness": { + "type": 3 + }, + "seed": 1318000125, + "version": 1100, + "versionNonce": 1610752094, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "pGewt0iJ9suIPW-Ife0Ph" + }, + { + "id": "O1A6koapuKvLTUJaEVbme", + "type": "arrow" + } + ], + "updated": 1760169932826, + "link": null, + "locked": false + }, + { + "id": "pGewt0iJ9suIPW-Ife0Ph", + "type": "text", + "x": -621.1102437707624, + "y": 4.235558114528715, + "width": 142.5598907470703, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0k", + "roundness": null, + "seed": 1683278675, + "version": 1012, + "versionNonce": 1507484830, + "isDeleted": false, + "boundElements": [], + "updated": 1760169932826, + "link": null, + "locked": false, + "text": "audio producer", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "bYEREuhUrR7Oa81xDvjOG", + "originalText": "audio producer", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "O1A6koapuKvLTUJaEVbme", + "type": "arrow", + "x": -446.1393014575337, + "y": 4.432943285342676, + "width": 424.6958135254392, + "height": 18.419214095350526, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0l", + "roundness": { + "type": 2 + }, + "seed": 613901075, + "version": 2779, + "versionNonce": 903978334, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "JnXo2GoFelztQn81xHDp3" + } + ], + "updated": 1760169959585, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 424.6958135254392, + -18.419214095350526 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "bYEREuhUrR7Oa81xDvjOG", + "mode": "orbit", + "fixedPoint": [ + 0.6672455214784396, + 0.3327544785215605 + ] + }, + "endBinding": { + "elementId": "FOpt86bwZ_q6IBi3rvYOB", + "mode": "orbit", + "fixedPoint": [ + 0.24763805788336948, + 0.7523619421166304 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "JnXo2GoFelztQn81xHDp3", + "type": "text", + "x": -292.11134861327116, + "y": -29.77666376233259, + "width": 116.63990783691406, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0m", + "roundness": null, + "seed": 897799229, + "version": 26, + "versionNonce": 400604382, + "isDeleted": false, + "boundElements": [], + "updated": 1760169959021, + "link": null, + "locked": false, + "text": "write audio \nchunks", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "O1A6koapuKvLTUJaEVbme", + "originalText": "write audio \nchunks", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "Ds4RPQESmSUCtE6t82zuk", + "type": "rectangle", + "x": -652.6919927231843, + "y": -92.5020725581752, + "width": 199.27543254497436, + "height": 60, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0o", + "roundness": { + "type": 3 + }, + "seed": 1192893117, + "version": 1382, + "versionNonce": 995081182, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "7zlUn1rKkNbbBw78SDlyy" + }, + { + "id": "b5c1RztwZGpwzDEpSEnsO", + "type": "arrow" + }, + { + "id": "vWM-NSvg82C_Qe5tXYFOR", + "type": "arrow" + } + ], + "updated": 1760169891043, + "link": null, + "locked": false + }, + { + "id": "7zlUn1rKkNbbBw78SDlyy", + "type": "text", + "x": -626.1142206035291, + "y": -75.0020725581752, + "width": 146.11988830566406, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0p", + "roundness": null, + "seed": 1560823091, + "version": 1376, + "versionNonce": 1072205854, + "isDeleted": false, + "boundElements": [], + "updated": 1760169891043, + "link": null, + "locked": false, + "text": "audio consumer", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Ds4RPQESmSUCtE6t82zuk", + "originalText": "audio consumer", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "b5c1RztwZGpwzDEpSEnsO", + "type": "arrow", + "x": -561.9564962488615, + "y": -93.50207255817519, + "width": 4.40053236273252, + "height": 93.33682567193775, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0s", + "roundness": { + "type": 2 + }, + "seed": 371961651, + "version": 1382, + "versionNonce": 1598826370, + "isDeleted": false, + "boundElements": [], + "updated": 1760169895896, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -4.40053236273252, + -93.33682567193775 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "Ds4RPQESmSUCtE6t82zuk", + "mode": "orbit", + "fixedPoint": [ + 0.4621237111881557, + 0.46212371118815626 + ] + }, + "endBinding": { + "elementId": "qw4FSQEULfB3EwHremgmE", + "mode": "orbit", + "fixedPoint": [ + 0.41888495497736167, + 0.581115045022638 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "mqOfRORnnRQ0pX9K4FzXa", + "type": "rectangle", + "x": -216.75589923798225, + "y": 486.09782179784474, + "width": 0.6497513412627995, + "height": 0.3193693033326781, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0t", + "roundness": { + "type": 3 + }, + "seed": 1832165149, + "version": 75, + "versionNonce": 627649065, + "isDeleted": false, + "boundElements": [], + "updated": 1761730620765, + "link": null, + "locked": false + }, + { + "id": "qw4FSQEULfB3EwHremgmE", + "type": "rectangle", + "x": -695.2508959535205, + "y": -348.2923888193958, + "width": 300, + "height": 160, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "PwmF54b90a3V0RnBXxGHD" + ], + "frameId": null, + "index": "b0uG", + "roundness": { + "type": 3 + }, + "seed": 979033232, + "version": 766, + "versionNonce": 198030174, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "ASvTkIpEKnPfAxCtmo8tA" + }, + { + "id": "b5c1RztwZGpwzDEpSEnsO", + "type": "arrow" + }, + { + "id": "9oBCVc6ESpSpM3q6gAStp", + "type": "arrow" + }, + { + "id": "04xIJKT9TRHJvVeTLZiQU", + "type": "arrow" + } + ], + "updated": 1760174126458, + "link": null, + "locked": false + }, + { + "id": "ASvTkIpEKnPfAxCtmo8tA", + "type": "text", + "x": -660.7508959535205, + "y": -343.2923888193958, + "width": 231, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "PwmF54b90a3V0RnBXxGHD" + ], + "frameId": null, + "index": "b0uV", + "roundness": null, + "seed": 32557712, + "version": 780, + "versionNonce": 326779998, + "isDeleted": false, + "boundElements": [], + "updated": 1760169918902, + "link": null, + "locked": false, + "text": "audio persistance job", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "top", + "containerId": "qw4FSQEULfB3EwHremgmE", + "originalText": "audio persistance job", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "XeG35glLjTFOqD4QRBUnG", + "type": "rectangle", + "x": -643.2194834055579, + "y": -268.1225654037495, + "width": 180, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "PwmF54b90a3V0RnBXxGHD" + ], + "frameId": null, + "index": "b0v", + "roundness": { + "type": 3 + }, + "seed": 1402026640, + "version": 710, + "versionNonce": 729888898, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "uf5UNCoO4SL8nlK6DqUxS" + } + ], + "updated": 1760169918902, + "link": null, + "locked": false + }, + { + "id": "uf5UNCoO4SL8nlK6DqUxS", + "type": "text", + "x": -628.0193796457922, + "y": -263.1225654037495, + "width": 149.59979248046875, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "PwmF54b90a3V0RnBXxGHD" + ], + "frameId": null, + "index": "b0vG", + "roundness": null, + "seed": 548104336, + "version": 649, + "versionNonce": 850266270, + "isDeleted": false, + "boundElements": [], + "updated": 1760169918902, + "link": null, + "locked": false, + "text": "Write audio chunk\nto WAV file", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "XeG35glLjTFOqD4QRBUnG", + "originalText": "Write audio chunk to WAV file", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "wWjaoLLJ7A7pvMcIe4ppW", + "type": "text", + "x": -540.4391889690592, + "y": -173.2214402404325, + "width": 273.3191253548492, + "height": 38.27143765347341, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "PwmF54b90a3V0RnBXxGHD" + ], + "frameId": null, + "index": "b0vV", + "roundness": null, + "seed": 1512803261, + "version": 1122, + "versionNonce": 495638594, + "isDeleted": false, + "boundElements": [], + "updated": 1760169918902, + "link": null, + "locked": false, + "text": "This writes to disk so no lost audio\nif session closes", + "fontSize": 15.308575061389366, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "This writes to disk so no lost audio\nif session closes", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "fL6YA-UBRXzcwAF3_FA5t", + "type": "arrow", + "x": 416.39733173989197, + "y": -32.72144207471307, + "width": 267.12493189821896, + "height": 8.054197039452887, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0w", + "roundness": { + "type": 2 + }, + "seed": 1717585341, + "version": 2302, + "versionNonce": 320253406, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "8WSI4Krn05ASfEdLwKbPY" + } + ], + "updated": 1760171099172, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -267.12493189821896, + -8.054197039452887 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "3WtKjlEwj_rAIEMF2zwR4", + "mode": "orbit", + "fixedPoint": [ + 0.3964014595749674, + 0.6035985404250327 + ] + }, + "endBinding": { + "elementId": "FOpt86bwZ_q6IBi3rvYOB", + "mode": "orbit", + "fixedPoint": [ + 0.5896893788593741, + 0.41031062114062583 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "8WSI4Krn05ASfEdLwKbPY", + "type": "text", + "x": 261.73488257545046, + "y": -49.276494790851466, + "width": 42.19996643066406, + "height": 25, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0wG", + "roundness": null, + "seed": 1853094110, + "version": 7, + "versionNonce": 1130271134, + "isDeleted": false, + "boundElements": [], + "updated": 1760171099172, + "link": null, + "locked": false, + "text": "read", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "fL6YA-UBRXzcwAF3_FA5t", + "originalText": "read", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "vWM-NSvg82C_Qe5tXYFOR", + "type": "arrow", + "x": -451.2030005240761, + "y": -56.3401309051924, + "width": 435.8295108659675, + "height": 2.447186976347581, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0y", + "roundness": { + "type": 2 + }, + "seed": 437406338, + "version": 1697, + "versionNonce": 488912286, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "owphDJoxgr0dpYj8o7x-C" + } + ], + "updated": 1760169948323, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 435.8295108659675, + -2.447186976347581 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "Ds4RPQESmSUCtE6t82zuk", + "mode": "orbit", + "fixedPoint": [ + 0.6101759664218374, + 0.6101759664218378 + ] + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "owphDJoxgr0dpYj8o7x-C", + "type": "text", + "x": -254.38822830642437, + "y": -70.06372439336619, + "width": 42.19996643066406, + "height": 25, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0yV", + "roundness": null, + "seed": 1199577950, + "version": 7, + "versionNonce": 155215134, + "isDeleted": false, + "boundElements": [], + "updated": 1760169947380, + "link": null, + "locked": false, + "text": "read", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "vWM-NSvg82C_Qe5tXYFOR", + "originalText": "read", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "AWIFdEtCDkqRy5aN0ZMkb", + "type": "text", + "x": 164.3427870785083, + "y": -288.3109299420281, + "width": 279.92413330078125, + "height": 28.226731237119434, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b10", + "roundness": null, + "seed": 179577694, + "version": 196, + "versionNonce": 714593246, + "isDeleted": false, + "boundElements": [], + "updated": 1760168336832, + "link": null, + "locked": false, + "text": "This is the file metadata", + "fontSize": 22.581384989695547, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "This is the file metadata", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "rHa_v2VzCQ6VnF44ywF8F", + "type": "rectangle", + "x": -190.49653808228777, + "y": -145.82480145738708, + "width": 491.27494525518847, + "height": 331.87291622227724, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b10G", + "roundness": { + "type": 3 + }, + "seed": 1461154448, + "version": 2554, + "versionNonce": 2106612290, + "isDeleted": false, + "boundElements": [ + { + "id": "GSJo6Q9_qu4Tj6kM7Vf_6", + "type": "text" + } + ], + "updated": 1760169939480, + "link": null, + "locked": false + }, + { + "id": "GSJo6Q9_qu4Tj6kM7Vf_6", + "type": "text", + "x": -53.021655481549004, + "y": -140.82480145738708, + "width": 216.32518005371094, + "height": 42.72716286252441, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b10V", + "roundness": null, + "seed": 1185587344, + "version": 3543, + "versionNonce": 636150494, + "isDeleted": false, + "boundElements": [], + "updated": 1760169939480, + "link": null, + "locked": false, + "text": "Redis stream", + "fontSize": 34.18173029001953, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": "rHa_v2VzCQ6VnF44ywF8F", + "originalText": "Redis stream", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "FOpt86bwZ_q6IBi3rvYOB", + "type": "rectangle", + "x": -20.763421650626356, + "y": -75.40429940428606, + "width": 168.260515714727, + "height": 79.19245604553446, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b10l", + "roundness": { + "type": 3 + }, + "seed": 2007502480, + "version": 3416, + "versionNonce": 1593576578, + "isDeleted": false, + "boundElements": [ + { + "id": "xJCudECerB0TZyUuc45lR", + "type": "arrow" + }, + { + "id": "O1A6koapuKvLTUJaEVbme", + "type": "arrow" + }, + { + "id": "VN3tPxcAVaaLV7T8ex8aM", + "type": "text" + }, + { + "id": "fL6YA-UBRXzcwAF3_FA5t", + "type": "arrow" + } + ], + "updated": 1760169946406, + "link": null, + "locked": false + }, + { + "id": "VN3tPxcAVaaLV7T8ex8aM", + "type": "text", + "x": -12.280067784962057, + "y": -62.25821982022444, + "width": 151.29380798339844, + "height": 52.90029687741117, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b11", + "roundness": null, + "seed": 1120386192, + "version": 4352, + "versionNonce": 445044510, + "isDeleted": false, + "boundElements": [], + "updated": 1760169939480, + "link": null, + "locked": false, + "text": "audio:stream:\nclient_id", + "fontSize": 21.160118750964468, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "FOpt86bwZ_q6IBi3rvYOB", + "originalText": "audio:stream:client_id", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "xCMqUUiSZfBbjazMoufce", + "type": "text", + "x": -29.06285792590438, + "y": -204.64649690755977, + "width": 163.25985717773438, + "height": 25, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b11V", + "roundness": null, + "seed": 775057922, + "version": 192, + "versionNonce": 1464611266, + "isDeleted": false, + "boundElements": [], + "updated": 1760169939480, + "link": null, + "locked": false, + "text": "Session strorage", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Session strorage", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "oJLfhhXdXz0V_w8uaIwMj", + "type": "rectangle", + "x": -17.213155498804554, + "y": 63.08340560066574, + "width": 163.84195897694667, + "height": 73.50711898405144, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b12", + "roundness": { + "type": 3 + }, + "seed": 1831484418, + "version": 478, + "versionNonce": 887234206, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "TZ9wzSCE4_nIjRG7wP59t" + }, + { + "id": "Loa1G4nM5mxY_WW_vp6a_", + "type": "arrow" + } + ], + "updated": 1760171155923, + "link": null, + "locked": false + }, + { + "id": "TZ9wzSCE4_nIjRG7wP59t", + "type": "text", + "x": 2.607886550703938, + "y": 74.83696509269146, + "width": 124.19987487792969, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b12V", + "roundness": null, + "seed": 1210716674, + "version": 467, + "versionNonce": 539273602, + "isDeleted": false, + "boundElements": [], + "updated": 1760169939480, + "link": null, + "locked": false, + "text": "transcription\nresults", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "oJLfhhXdXz0V_w8uaIwMj", + "originalText": "transcription results", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "wyDwwAPLPXarx3NIsaVRd", + "type": "arrow", + "x": 518.611446351194, + "y": 36.757463544917414, + "width": 374.6739713626356, + "height": 58.53865381741923, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b14", + "roundness": { + "type": 2 + }, + "seed": 907359490, + "version": 1055, + "versionNonce": 205643806, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "voMssVVj8-teXt6oNQatT" + } + ], + "updated": 1760171137978, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -374.6739713626356, + 58.53865381741923 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "50aqoS0EVA2qgHUbN07nt", + "mode": "inside", + "fixedPoint": [ + 0.21411854814689113, + 0.7002836740682671 + ] + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "voMssVVj8-teXt6oNQatT", + "type": "text", + "x": 311.60247122895817, + "y": 56.02679045362703, + "width": 39.34397888183594, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b141", + "roundness": null, + "seed": 1539988254, + "version": 18, + "versionNonce": 481042754, + "isDeleted": false, + "boundElements": [], + "updated": 1760171136018, + "link": null, + "locked": false, + "text": "write", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "wyDwwAPLPXarx3NIsaVRd", + "originalText": "write", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "Rtq9K_aCm1ily8GPpv06z", + "type": "rectangle", + "x": 427.9015457149376, + "y": 599.6569559470396, + "width": 190.82866510855592, + "height": 55, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1A", + "roundness": { + "type": 3 + }, + "seed": 1989695902, + "version": 289, + "versionNonce": 2071428199, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "VtpIkHeu2GSY0uoqnwKj9" + }, + { + "id": "Ftv81G0u9I6JpAAOo0BwB", + "type": "arrow" + } + ], + "updated": 1761730941504, + "link": null, + "locked": false + }, + { + "id": "VtpIkHeu2GSY0uoqnwKj9", + "type": "text", + "x": 477.7879165382585, + "y": 617.1569559470396, + "width": 91.05592346191406, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1B", + "roundness": null, + "seed": 1272411614, + "version": 272, + "versionNonce": 2017023879, + "isDeleted": false, + "boundElements": [], + "updated": 1761730941504, + "link": null, + "locked": false, + "text": "Add memory", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Rtq9K_aCm1ily8GPpv06z", + "originalText": "Add memory", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "voHx5fMo3BdrfPC2NZ8XQ", + "type": "rectangle", + "x": -1452.1779799434805, + "y": -374.9150748181636, + "width": 189.14922308240966, + "height": 54.13309691488371, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1C", + "roundness": { + "type": 3 + }, + "seed": 774436418, + "version": 95, + "versionNonce": 1959621854, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "kXBnSzJsA4xQ8qHRR7hXu" + } + ], + "updated": 1760169292928, + "link": null, + "locked": false + }, + { + "id": "kXBnSzJsA4xQ8qHRR7hXu", + "type": "text", + "x": -1371.3953529603812, + "y": -357.84852636072173, + "width": 27.583969116210938, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1D", + "roundness": null, + "seed": 85496542, + "version": 7, + "versionNonce": 1397172510, + "isDeleted": false, + "boundElements": [], + "updated": 1760169292928, + "link": null, + "locked": false, + "text": "Job", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "voHx5fMo3BdrfPC2NZ8XQ", + "originalText": "Job", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "GqT3L4lg2JXVAuIGleNpr", + "type": "rectangle", + "x": -1447.830702961047, + "y": -298.65464091289647, + "width": 189.14922308240966, + "height": 54.13309691488371, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1E", + "roundness": { + "type": 3 + }, + "seed": 753464798, + "version": 143, + "versionNonce": 1514037918, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "3YCJNUKC2s0Y2vhy4aFht" + } + ], + "updated": 1760170362268, + "link": null, + "locked": false + }, + { + "id": "3YCJNUKC2s0Y2vhy4aFht", + "type": "text", + "x": -1391.0080598036313, + "y": -281.5880924554546, + "width": 75.50393676757812, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1F", + "roundness": null, + "seed": 1581943326, + "version": 64, + "versionNonce": 1070882526, + "isDeleted": false, + "boundElements": [], + "updated": 1760170362268, + "link": null, + "locked": false, + "text": "Controller", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "GqT3L4lg2JXVAuIGleNpr", + "originalText": "Controller", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "STd_bGkioO2W99oLPv3LA", + "type": "rectangle", + "x": -729.134136749316, + "y": 128.69452514997863, + "width": 189.14922308240966, + "height": 54.13309691488371, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1I", + "roundness": { + "type": 3 + }, + "seed": 601135298, + "version": 439, + "versionNonce": 477782302, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "7XqkUvyeh7THYXyGIHPuT" + }, + { + "id": "GO5h3hI0Wsmx1sviPag__", + "type": "arrow" + }, + { + "id": "FGuvfBKZSJQlBNEEyzmds", + "type": "arrow" + } + ], + "updated": 1760171262661, + "link": null, + "locked": false + }, + { + "id": "7XqkUvyeh7THYXyGIHPuT", + "type": "text", + "x": -716.5274588628963, + "y": 145.76107360742049, + "width": 163.9358673095703, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1J", + "roundness": null, + "seed": 755511426, + "version": 370, + "versionNonce": 118263134, + "isDeleted": false, + "boundElements": [], + "updated": 1760171262661, + "link": null, + "locked": false, + "text": "Speech detection job", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "STd_bGkioO2W99oLPv3LA", + "originalText": "Speech detection job", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "9oBCVc6ESpSpM3q6gAStp", + "type": "arrow", + "x": -935.6036381702245, + "y": -61.80100473699622, + "width": 243.60168930062628, + "height": 218.97170837292043, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1K", + "roundness": { + "type": 2 + }, + "seed": 696303006, + "version": 77, + "versionNonce": 480017950, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Bvex2f5y2vTnRG168uXGh" + } + ], + "updated": 1760170015271, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 243.60168930062628, + -218.97170837292043 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "xnZp105apVNOn_w5xpohm", + "mode": "inside", + "fixedPoint": [ + 0.9255522938206991, + 0.01852378640890644 + ] + }, + "endBinding": { + "elementId": "qw4FSQEULfB3EwHremgmE", + "mode": "inside", + "fixedPoint": [ + 0.010829823613074344, + 0.42199797318424465 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "Bvex2f5y2vTnRG168uXGh", + "type": "text", + "x": -853.4907741717668, + "y": -181.28685892345644, + "width": 79.37596130371094, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1L", + "roundness": null, + "seed": 1508915330, + "version": 21, + "versionNonce": 177922882, + "isDeleted": false, + "boundElements": [], + "updated": 1760170014196, + "link": null, + "locked": false, + "text": "ENQUEUE", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "9oBCVc6ESpSpM3q6gAStp", + "originalText": "ENQUEUE", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "GO5h3hI0Wsmx1sviPag__", + "type": "arrow", + "x": -925.4881173824574, + "y": 165.4341412152302, + "width": 192.43620030001898, + "height": 0.650783973445499, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1M", + "roundness": { + "type": 2 + }, + "seed": 295055938, + "version": 153, + "versionNonce": 1863266718, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "_EuYi7h-V5qDV30mlWTXz" + } + ], + "updated": 1760171262662, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 192.43620030001898, + 0.650783973445499 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "xnZp105apVNOn_w5xpohm", + "mode": "orbit", + "fixedPoint": [ + 0.7756913093249034, + 0.7756913093249016 + ] + }, + "endBinding": { + "elementId": "STd_bGkioO2W99oLPv3LA", + "mode": "orbit", + "fixedPoint": [ + 0.3054336891912888, + 0.6945663108087112 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "_EuYi7h-V5qDV30mlWTXz", + "type": "text", + "x": -826.96935607691, + "y": 158.78913863647398, + "width": 79.37596130371094, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1N", + "roundness": null, + "seed": 511435714, + "version": 10, + "versionNonce": 1023234114, + "isDeleted": false, + "boundElements": [], + "updated": 1760170804216, + "link": null, + "locked": false, + "text": "ENQUEUE", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "GO5h3hI0Wsmx1sviPag__", + "originalText": "ENQUEUE", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "E0Rv8FO_LZEWkbVqI6Q5U", + "type": "rectangle", + "x": -1445.5730923685253, + "y": -220.55783351346446, + "width": 189.14922308240966, + "height": 54.13309691488371, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1Q", + "roundness": { + "type": 3 + }, + "seed": 788603806, + "version": 188, + "versionNonce": 1739142978, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "sfHdFoqdVEPJM0yZdgmEj" + } + ], + "updated": 1760170366856, + "link": null, + "locked": false + }, + { + "id": "sfHdFoqdVEPJM0yZdgmEj", + "type": "text", + "x": -1376.9984655685314, + "y": -203.4912850560226, + "width": 51.999969482421875, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1R", + "roundness": null, + "seed": 1352073182, + "version": 115, + "versionNonce": 734870430, + "isDeleted": false, + "boundElements": [], + "updated": 1760170371618, + "link": null, + "locked": false, + "text": "Worker", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "E0Rv8FO_LZEWkbVqI6Q5U", + "originalText": "Worker", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "lLH87OlNjmzx1sShAwo0I", + "type": "text", + "x": 957.2211335401921, + "y": -92.9009672408364, + "width": 243.23826905890292, + "height": 60, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1Y", + "roundness": null, + "seed": 1239121054, + "version": 236, + "versionNonce": 1678448770, + "isDeleted": false, + "boundElements": [], + "updated": 1760171071671, + "link": null, + "locked": false, + "text": "Accumulates ~5sec of audio\nbefore calling provider\ntranscription", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Accumulates ~5sec of audio before calling provider transcription", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "yML9_fBQlNtYR42h_UwS6", + "type": "rectangle", + "x": -330.08731227617454, + "y": 333.26936808283597, + "width": 189.14922308240966, + "height": 54.13309691488371, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1Z", + "roundness": { + "type": 3 + }, + "seed": 1969749634, + "version": 783, + "versionNonce": 987486055, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "EimYhGt_jukHGn37zSdWR" + }, + { + "id": "hbAAGoUIsWDseip5zX4zQ", + "type": "arrow" + }, + { + "id": "hlT4WMPl5NpaoX4jmIKQq", + "type": "arrow" + }, + { + "id": "FpjK5TBBvXyG6977AosyI", + "type": "arrow" + } + ], + "updated": 1761730694393, + "link": null, + "locked": false + }, + { + "id": "EimYhGt_jukHGn37zSdWR", + "type": "text", + "x": -306.6566445215908, + "y": 340.3359165402778, + "width": 142.2878875732422, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1a", + "roundness": null, + "seed": 1252580930, + "version": 727, + "versionNonce": 1729052009, + "isDeleted": false, + "boundElements": [], + "updated": 1761730694393, + "link": null, + "locked": false, + "text": "Open conversation\n job", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "yML9_fBQlNtYR42h_UwS6", + "originalText": "Open conversation\n job", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "xyKZb3H2HwpA8Dtpoa1kZ", + "type": "rectangle", + "x": -19.595469181436783, + "y": 495.7780051008122, + "width": 189.14922308240966, + "height": 54.13309691488371, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1b", + "roundness": { + "type": 3 + }, + "seed": 490677790, + "version": 810, + "versionNonce": 1178471753, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "lnMpATMtM82GtQv0YUd5h" + }, + { + "id": "bKLI1IzWYBb_VX9tuX2KT", + "type": "arrow" + }, + { + "id": "CGkRCgPddwVHSAIqsMdAe", + "type": "arrow" + } + ], + "updated": 1761730846027, + "link": null, + "locked": false + }, + { + "id": "lnMpATMtM82GtQv0YUd5h", + "type": "text", + "x": -10.284773655856952, + "y": 502.84455355825406, + "width": 170.52783203125, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1c", + "roundness": null, + "seed": 2096149086, + "version": 824, + "versionNonce": 1224404711, + "isDeleted": false, + "boundElements": [], + "updated": 1761730798622, + "link": null, + "locked": false, + "text": "split\naudio_persistance file", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "xyKZb3H2HwpA8Dtpoa1kZ", + "originalText": "split audio_persistance file", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "Loa1G4nM5mxY_WW_vp6a_", + "type": "arrow", + "x": -234.54396641666946, + "y": 201.6947455889964, + "width": 216.36685694978647, + "height": 81.86631308289013, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1f", + "roundness": { + "type": 2 + }, + "seed": 725572418, + "version": 188, + "versionNonce": 1429988702, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "OpDjJGJ175BSAGd06KP28" + } + ], + "updated": 1760171277868, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 216.36685694978647, + -81.86631308289013 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "axoaqdleYlBvKQuqjwLoL", + "mode": "orbit", + "fixedPoint": [ + 0.808094462631756, + 0.8080944626317561 + ] + }, + "endBinding": { + "elementId": "oJLfhhXdXz0V_w8uaIwMj", + "mode": "orbit", + "fixedPoint": [ + 0.41609171734036565, + 0.41609171734036565 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "OpDjJGJ175BSAGd06KP28", + "type": "text", + "x": -144.4159595093181, + "y": 149.18603406730045, + "width": 33.75996398925781, + "height": 20, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1g", + "roundness": null, + "seed": 552890754, + "version": 7, + "versionNonce": 636219778, + "isDeleted": false, + "boundElements": [], + "updated": 1760171274298, + "link": null, + "locked": false, + "text": "read", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Loa1G4nM5mxY_WW_vp6a_", + "originalText": "read", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "axoaqdleYlBvKQuqjwLoL", + "type": "rectangle", + "x": -425.55158846243853, + "y": 172.38754966626414, + "width": 189.14922308240966, + "height": 54.13309691488371, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "AKSetNGknztZqW1iz2bJX" + ], + "frameId": null, + "index": "b1gV", + "roundness": { + "type": 3 + }, + "seed": 1834292702, + "version": 576, + "versionNonce": 1739441438, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "vrpAt6CKvImM2sXdSYeGR" + }, + { + "id": "Loa1G4nM5mxY_WW_vp6a_", + "type": "arrow" + } + ], + "updated": 1760171285850, + "link": null, + "locked": false + }, + { + "id": "vrpAt6CKvImM2sXdSYeGR", + "type": "text", + "x": -369.82493834701495, + "y": 189.454098123706, + "width": 77.6959228515625, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "AKSetNGknztZqW1iz2bJX" + ], + "frameId": null, + "index": "b1h", + "roundness": null, + "seed": 2083695134, + "version": 494, + "versionNonce": 114500546, + "isDeleted": false, + "boundElements": [], + "updated": 1760171285850, + "link": null, + "locked": false, + "text": "Agregator", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "axoaqdleYlBvKQuqjwLoL", + "originalText": "Agregator", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "GcRjzNQ5Yvnpv9LIrKsMK", + "type": "text", + "x": -437.49765231727065, + "y": 138.23981286251785, + "width": 241.31178283691406, + "height": 20, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "AKSetNGknztZqW1iz2bJX" + ], + "frameId": null, + "index": "b1hV", + "roundness": null, + "seed": 1262868098, + "version": 161, + "versionNonce": 1580429662, + "isDeleted": false, + "boundElements": [], + "updated": 1760171285850, + "link": null, + "locked": false, + "text": "agregates to create sentences", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "agregates to create sentences", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "FGuvfBKZSJQlBNEEyzmds", + "type": "arrow", + "x": -538.9849136669063, + "y": 162.8049784215547, + "width": 115.09900131236532, + "height": 17.899939049607696, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1i", + "roundness": { + "type": 2 + }, + "seed": 2070460866, + "version": 138, + "versionNonce": 443347586, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "XyY1cI76GKlg0yu8XEegC" + } + ], + "updated": 1760171323702, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 115.09900131236532, + 17.899939049607696 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "STd_bGkioO2W99oLPv3LA", + "mode": "orbit", + "fixedPoint": [ + 0.5935933380468639, + 0.4064066619531361 + ] + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "XyY1cI76GKlg0yu8XEegC", + "type": "text", + "x": -494.595401414044, + "y": 161.75494794635853, + "width": 26.319976806640625, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1iV", + "roundness": null, + "seed": 1775544542, + "version": 5, + "versionNonce": 1647926814, + "isDeleted": false, + "boundElements": [], + "updated": 1760171321768, + "link": null, + "locked": false, + "text": "get", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "FGuvfBKZSJQlBNEEyzmds", + "originalText": "get", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "q6OSkiFHGDXLFPYDu-OGX", + "type": "diamond", + "x": -714.8022116101575, + "y": 317.7803037294093, + "width": 186.77597877661017, + "height": 116.85123187568198, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1k", + "roundness": { + "type": 2 + }, + "seed": 1221576386, + "version": 680, + "versionNonce": 318089289, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "oL7-5-dDUSRF42mXS9HJX" + }, + { + "id": "2r8VBkBp0f-l_AU5vdG6R", + "type": "arrow" + }, + { + "id": "NuXQqOfu6Jj2NoAWRoRx2", + "type": "arrow" + }, + { + "id": "FpjK5TBBvXyG6977AosyI", + "type": "arrow" + } + ], + "updated": 1761730694393, + "link": null, + "locked": false + }, + { + "id": "oL7-5-dDUSRF42mXS9HJX", + "type": "text", + "x": -662.5761810883682, + "y": 355.9931116983298, + "width": 81.93592834472656, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1l", + "roundness": null, + "seed": 1790183582, + "version": 604, + "versionNonce": 948186142, + "isDeleted": false, + "boundElements": [], + "updated": 1760171493220, + "link": null, + "locked": false, + "text": "worthwhile\nspeech?", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "q6OSkiFHGDXLFPYDu-OGX", + "originalText": "worthwhile speech?", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "2r8VBkBp0f-l_AU5vdG6R", + "type": "arrow", + "x": -628.5085233526924, + "y": 178.63227453753342, + "width": 7.066006199163098, + "height": 147.34275133236048, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1m", + "roundness": { + "type": 2 + }, + "seed": 658029698, + "version": 550, + "versionNonce": 1517206473, + "isDeleted": false, + "boundElements": [], + "updated": 1761730694395, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -7.066006199163098, + 147.34275133236048 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "q6OSkiFHGDXLFPYDu-OGX", + "mode": "orbit", + "fixedPoint": [ + 0.411288416910216, + 0.5001 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "FpjK5TBBvXyG6977AosyI", + "type": "arrow", + "x": -531.2709053206735, + "y": 376.4107829161594, + "width": 200.2623194668493, + "height": 0.5151691068164155, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1o", + "roundness": { + "type": 2 + }, + "seed": 2082456002, + "version": 528, + "versionNonce": 1042750857, + "isDeleted": false, + "boundElements": [], + "updated": 1761730694395, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 200.2623194668493, + -0.5151691068164155 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "q6OSkiFHGDXLFPYDu-OGX", + "mode": "orbit", + "fixedPoint": [ + 0.5001, + 0.5037376958240032 + ] + }, + "endBinding": { + "elementId": "yML9_fBQlNtYR42h_UwS6", + "mode": "orbit", + "fixedPoint": [ + 0.21453809140281266, + 0.7854619085971858 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "WDwn2MFwLWSIH2KjFwFZY", + "type": "rectangle", + "x": 189.48149782369796, + "y": 334.14442635760014, + "width": 165.69297117984706, + "height": 57.36267089773732, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1p", + "roundness": { + "type": 3 + }, + "seed": 1091835486, + "version": 284, + "versionNonce": 35321289, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "arpiKJFOuD8k6dq09Sqrr" + }, + { + "id": "hbAAGoUIsWDseip5zX4zQ", + "type": "arrow" + } + ], + "updated": 1761730620766, + "link": null, + "locked": false + }, + { + "id": "arpiKJFOuD8k6dq09Sqrr", + "type": "text", + "x": 222.16002229301603, + "y": 352.8257618064688, + "width": 100.33592224121094, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1q", + "roundness": null, + "seed": 1191801090, + "version": 226, + "versionNonce": 1487623337, + "isDeleted": false, + "boundElements": [], + "updated": 1761730620766, + "link": null, + "locked": false, + "text": "Conversation", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "WDwn2MFwLWSIH2KjFwFZY", + "originalText": "Conversation", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "hbAAGoUIsWDseip5zX4zQ", + "type": "arrow", + "x": -139.9380891937649, + "y": 361.98257979218243, + "width": 327.6583737625558, + "height": 0.4951760508383245, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1r", + "roundness": { + "type": 2 + }, + "seed": 1682907970, + "version": 321, + "versionNonce": 277591849, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "_xmXoNR7Ev5JBFWGSDaQv" + } + ], + "updated": 1761730694394, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 327.6583737625558, + 0.4951760508383245 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "yML9_fBQlNtYR42h_UwS6", + "mode": "orbit", + "fixedPoint": [ + 0.527897907737186, + 0.5278979077371869 + ] + }, + "endBinding": { + "elementId": "WDwn2MFwLWSIH2KjFwFZY", + "mode": "orbit", + "fixedPoint": [ + 0.4961454519211681, + 0.4961454519211681 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "_xmXoNR7Ev5JBFWGSDaQv", + "type": "text", + "x": -190.83318247598106, + "y": 357.86977817324197, + "width": 51.583953857421875, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1s", + "roundness": null, + "seed": 1346227934, + "version": 8, + "versionNonce": 712675550, + "isDeleted": false, + "boundElements": [], + "updated": 1760171571529, + "link": null, + "locked": false, + "text": "Create", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "hbAAGoUIsWDseip5zX4zQ", + "originalText": "Create", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "W3UiHOcOTv4BFZ091kA_-", + "type": "text", + "x": -305.46114526107385, + "y": 281.61272256505646, + "width": 172.8318328857422, + "height": 40, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1t", + "roundness": null, + "seed": 1672705218, + "version": 197, + "versionNonce": 1942047049, + "isDeleted": false, + "boundElements": [], + "updated": 1761730620766, + "link": null, + "locked": false, + "text": "Append transcription\nAttach AudioFile path", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Append transcription\nAttach AudioFile path", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "G5J8OK3nUO2gMWEctVtYU", + "type": "rectangle", + "x": -837.7859832150489, + "y": 589.0307140005106, + "width": 173.02463755396388, + "height": 71.68093132885224, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "JizPEnpM3gtTA8lYOv4ew" + ], + "frameId": null, + "index": "b1u", + "roundness": { + "type": 3 + }, + "seed": 628021662, + "version": 152, + "versionNonce": 1266487399, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "FvbYIgiQjHdAKXHBrpA2J" + } + ], + "updated": 1761730630636, + "link": null, + "locked": false + }, + { + "id": "FvbYIgiQjHdAKXHBrpA2J", + "type": "text", + "x": -814.8816127412896, + "y": 614.8711796649367, + "width": 127.21589660644531, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "JizPEnpM3gtTA8lYOv4ew" + ], + "frameId": null, + "index": "b1v", + "roundness": null, + "seed": 1728520770, + "version": 135, + "versionNonce": 1517054855, + "isDeleted": false, + "boundElements": [], + "updated": 1761730630636, + "link": null, + "locked": false, + "text": "Close conversion", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "G5J8OK3nUO2gMWEctVtYU", + "originalText": "Close conversion", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "BLKDLJ6WY1IEhgyjKyp7p", + "type": "text", + "x": -820.6264779910305, + "y": 675.3256820322941, + "width": 181.9838409423828, + "height": 100, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "JizPEnpM3gtTA8lYOv4ew" + ], + "frameId": null, + "index": "b1w", + "roundness": null, + "seed": 1629426718, + "version": 272, + "versionNonce": 84107943, + "isDeleted": false, + "boundElements": [], + "updated": 1761730630636, + "link": null, + "locked": false, + "text": "Currently this is just \non client close?\nTODO: \n- Close on no-speech\n- Close on topic change", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Currently this is just \non client close?\nTODO: \n- Close on no-speech\n- Close on topic change", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "zJTDKMi5IG4WWLh70QxGG", + "type": "diamond", + "x": -287.3017416489747, + "y": 471.6086440791557, + "width": 125.7721374019527, + "height": 100, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1y", + "roundness": { + "type": 2 + }, + "seed": 1531143902, + "version": 151, + "versionNonce": 1850708583, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "rJotemamRIRYUrxI_u96n" + } + ], + "updated": 1761730713466, + "link": null, + "locked": false + }, + { + "id": "rJotemamRIRYUrxI_u96n", + "type": "text", + "x": -248.4706839220217, + "y": 501.6086440791557, + "width": 48.22395324707031, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1z", + "roundness": null, + "seed": 710206238, + "version": 101, + "versionNonce": 1134472583, + "isDeleted": false, + "boundElements": [], + "updated": 1761730713466, + "link": null, + "locked": false, + "text": "closed\n?", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "zJTDKMi5IG4WWLh70QxGG", + "originalText": "closed?", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "hlT4WMPl5NpaoX4jmIKQq", + "type": "arrow", + "x": -235.7300983733638, + "y": 389.64810500130324, + "width": 11.661205365783502, + "height": 87.28003051188466, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b20", + "roundness": { + "type": 2 + }, + "seed": 383785282, + "version": 315, + "versionNonce": 1379723785, + "isDeleted": false, + "boundElements": [], + "updated": 1761730694395, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 11.661205365783502, + 87.28003051188466 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "yML9_fBQlNtYR42h_UwS6", + "mode": "orbit", + "fixedPoint": [ + 0.47895066732240743, + 0.5210493326775929 + ] + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "bKLI1IzWYBb_VX9tuX2KT", + "type": "arrow", + "x": -169.70150346117424, + "y": 523.9266786548365, + "width": 147.57539772330824, + "height": 0.31620243308066165, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b21", + "roundness": { + "type": 2 + }, + "seed": 1720839966, + "version": 498, + "versionNonce": 509665417, + "isDeleted": false, + "boundElements": [], + "updated": 1761730769640, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 147.57539772330824, + 0.31620243308066165 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "xyKZb3H2HwpA8Dtpoa1kZ", + "mode": "orbit", + "fixedPoint": [ + 0.4705456933697614, + 0.5294543066302346 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "0Wz99-Bvc0YrDO5QxCmDN", + "type": "text", + "x": 177.41618044155416, + "y": 443.1067469518874, + "width": 212.75180053710938, + "height": 40, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b28", + "roundness": null, + "seed": 895403522, + "version": 123, + "versionNonce": 2140353054, + "isDeleted": false, + "boundElements": [], + "updated": 1760171915389, + "link": null, + "locked": false, + "text": "Runs through the whole \naudio file and retranscribes", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Runs through the whole \naudio file and retranscribes", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "CGkRCgPddwVHSAIqsMdAe", + "type": "arrow", + "x": 49.56370412993794, + "y": 552.3550158112264, + "width": 105.83357002897561, + "height": 110.24233555793626, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b29", + "roundness": { + "type": 2 + }, + "seed": 403329822, + "version": 695, + "versionNonce": 1782365065, + "isDeleted": false, + "boundElements": [], + "updated": 1761730932602, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -105.83357002897561, + 110.24233555793626 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "xyKZb3H2HwpA8Dtpoa1kZ", + "mode": "orbit", + "fixedPoint": [ + 0.5120889379720636, + 0.5120889379720625 + ] + }, + "endBinding": { + "elementId": "U3DFf7U24sN5v5OiE6Emp", + "mode": "orbit", + "fixedPoint": [ + 0.446223569663867, + 0.44622356966386695 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "Ftv81G0u9I6JpAAOo0BwB", + "type": "arrow", + "x": 336.24223699729276, + "y": 794.6156885921115, + "width": 221.95188871635213, + "height": 138.9587326450718, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2A", + "roundness": { + "type": 2 + }, + "seed": 1248975134, + "version": 512, + "versionNonce": 2062612103, + "isDeleted": false, + "boundElements": [], + "updated": 1761730947572, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 221.95188871635213, + -138.9587326450718 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "XKPgnbAxa6f_wEoGW2n-y", + "mode": "inside", + "fixedPoint": [ + 0.48584240990960725, + 0.016984809767902318 + ] + }, + "endBinding": { + "elementId": "Rtq9K_aCm1ily8GPpv06z", + "mode": "orbit", + "fixedPoint": [ + 0.7885051780426886, + 0.7885051780426879 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "NuXQqOfu6Jj2NoAWRoRx2", + "type": "arrow", + "x": -994.3418534873208, + "y": 384.4042264149942, + "width": 284.896515339767, + "height": 0.29382309441160714, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2C", + "roundness": { + "type": 2 + }, + "seed": 164412446, + "version": 108, + "versionNonce": 1050041001, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "_Hn2Iq0xOM1LzdlG0Qh97" + } + ], + "updated": 1761730694395, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 284.896515339767, + -0.29382309441160714 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "WjJhcq4N1a2nBEde3yVSl", + "mode": "orbit", + "fixedPoint": [ + 0.8076841165986415, + 0.19231588340135844 + ] + }, + "endBinding": { + "elementId": "q6OSkiFHGDXLFPYDu-OGX", + "mode": "orbit", + "fixedPoint": [ + 0.5001, + 0.5668687369299421 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "_Hn2Iq0xOM1LzdlG0Qh97", + "type": "text", + "x": -919.672724539748, + "y": 374.2724428003904, + "width": 135.58392333984375, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2D", + "roundness": null, + "seed": 1895939586, + "version": 18, + "versionNonce": 122380674, + "isDeleted": false, + "boundElements": [], + "updated": 1760173845092, + "link": null, + "locked": false, + "text": "Spoken by owner?", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "NuXQqOfu6Jj2NoAWRoRx2", + "originalText": "Spoken by owner?", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "cncAfkUT_i3iT3igpNuSP", + "type": "text", + "x": -919.313272536546, + "y": 343.25088500318594, + "width": 154.8478546142578, + "height": 20, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2E", + "roundness": null, + "seed": 1469869150, + "version": 36, + "versionNonce": 1069110622, + "isDeleted": false, + "boundElements": [], + "updated": 1760173861898, + "link": null, + "locked": false, + "text": "flag to turn this on", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "flag to turn this on", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "pgsWWiDCOIdvkgb3b33MV", + "type": "rectangle", + "x": -1025.3767697072822, + "y": 734.3848624139393, + "width": 100, + "height": 300, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2F", + "roundness": { + "type": 3 + }, + "seed": 201231298, + "version": 289, + "versionNonce": 1156756894, + "isDeleted": false, + "boundElements": [ + { + "id": "28MflYBmoYlCut7O6BYe2", + "type": "text" + }, + { + "id": "g4oobr9WVSN6N3Kg7PTVV", + "type": "arrow" + } + ], + "updated": 1760174367120, + "link": null, + "locked": false + }, + { + "id": "28MflYBmoYlCut7O6BYe2", + "type": "text", + "x": -1012.1287380910712, + "y": 844.3848624139393, + "width": 73.50393676757812, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2G", + "roundness": null, + "seed": 24888194, + "version": 323, + "versionNonce": 377901534, + "isDeleted": false, + "boundElements": [], + "updated": 1760174367120, + "link": null, + "locked": false, + "text": "/upload\n\naudio\ncontroller", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "pgsWWiDCOIdvkgb3b33MV", + "originalText": "/upload\n\naudio controller", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "g6fVFhCyiDTAO1zu6wO7R", + "type": "text", + "x": -537.0581422558828, + "y": 969.5220397505234, + "width": 159.95187377929688, + "height": 40, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2H", + "roundness": null, + "seed": 282917342, + "version": 253, + "versionNonce": 1127562409, + "isDeleted": false, + "boundElements": [], + "updated": 1761730878730, + "link": null, + "locked": false, + "text": "This creates a new \ntranscription version", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "This creates a new \ntranscription version", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "z__k3NCHvk401op7pyDyK", + "type": "rectangle", + "x": -139.79893709747512, + "y": 844.6265301822541, + "width": 189.14922308240966, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2I", + "roundness": { + "type": 3 + }, + "seed": 467039134, + "version": 970, + "versionNonce": 613122759, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "mXsSPwUp_hgOhFiEnpzX-" + }, + { + "id": "g4oobr9WVSN6N3Kg7PTVV", + "type": "arrow" + }, + { + "id": "Z1Q-w2TK07kWVCY3bkP0I", + "type": "arrow" + }, + { + "id": "37HWLa2RtB2HAhUTT_kCW", + "type": "arrow" + } + ], + "updated": 1761730938378, + "link": null, + "locked": false + }, + { + "id": "mXsSPwUp_hgOhFiEnpzX-", + "type": "text", + "x": -127.51225890587966, + "y": 849.6265301822541, + "width": 164.57586669921875, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2J", + "roundness": null, + "seed": 373444574, + "version": 968, + "versionNonce": 2036157001, + "isDeleted": false, + "boundElements": [], + "updated": 1761730930050, + "link": null, + "locked": false, + "text": "process transcription\njob", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "z__k3NCHvk401op7pyDyK", + "originalText": "process transcription\njob", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "g4oobr9WVSN6N3Kg7PTVV", + "type": "arrow", + "x": -921.1917037594012, + "y": 876.0800476239498, + "width": 780.3927666619261, + "height": 3.1382991074057145, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2K", + "roundness": { + "type": 2 + }, + "seed": 1453379778, + "version": 261, + "versionNonce": 1303320873, + "isDeleted": false, + "boundElements": [], + "updated": 1761730930050, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 780.3927666619261, + -3.1382991074057145 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "pgsWWiDCOIdvkgb3b33MV", + "mode": "orbit", + "fixedPoint": [ + 0.5269925597684607, + 0.4730074402315404 + ] + }, + "endBinding": { + "elementId": "z__k3NCHvk401op7pyDyK", + "mode": "orbit", + "fixedPoint": [ + 0.44047705160018086, + 0.5595229483998219 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "R4_czK_MfpvaRNEyHS3Sy", + "type": "text", + "x": 687.8625054731156, + "y": 512.7413606291149, + "width": 718.4517822265625, + "height": 385, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2M", + "roundness": null, + "seed": 1074593950, + "version": 448, + "versionNonce": 570712350, + "isDeleted": false, + "boundElements": [], + "updated": 1760174526411, + "link": null, + "locked": false, + "text": "This architecture effectively transcribes\nthe conversation twice, once with stream\nand one final pass.\nThe question may be why?\nDoing it this way means that we can build logic \ninto reacting with partial conversations\nand don't need to wait for the final version\n\nIf we wanted to improve the streaming transcription\nquality, we could continuously send the appended\nAudioFile for transcription every X seconds", + "fontSize": 28, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "This architecture effectively transcribes\nthe conversation twice, once with stream\nand one final pass.\nThe question may be why?\nDoing it this way means that we can build logic \ninto reacting with partial conversations\nand don't need to wait for the final version\n\nIf we wanted to improve the streaming transcription\nquality, we could continuously send the appended\nAudioFile for transcription every X seconds", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "U3DFf7U24sN5v5OiE6Emp", + "type": "rectangle", + "x": -159.11289373544264, + "y": 663.9823424805609, + "width": 196.5808791538434, + "height": 32.20144537260933, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2P", + "roundness": { + "type": 3 + }, + "seed": 112786887, + "version": 223, + "versionNonce": 447994313, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "L4zlmHf0rIS3uWuuaCKmf" + }, + { + "id": "CGkRCgPddwVHSAIqsMdAe", + "type": "arrow" + }, + { + "id": "Z1Q-w2TK07kWVCY3bkP0I", + "type": "arrow" + } + ], + "updated": 1761730932602, + "link": null, + "locked": false + }, + { + "id": "L4zlmHf0rIS3uWuuaCKmf", + "type": "text", + "x": -101.03041619465375, + "y": 670.0830651668655, + "width": 80.41592407226562, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2Q", + "roundness": null, + "seed": 655491017, + "version": 181, + "versionNonce": 881309865, + "isDeleted": false, + "boundElements": [], + "updated": 1761730932602, + "link": null, + "locked": false, + "text": "crop audio", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "U3DFf7U24sN5v5OiE6Emp", + "originalText": "crop audio", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "Z1Q-w2TK07kWVCY3bkP0I", + "type": "arrow", + "x": -67.37794977650456, + "y": 698.9119658639053, + "width": 5.332155922949113, + "height": 144.35639615162688, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2R", + "roundness": { + "type": 2 + }, + "seed": 1868855015, + "version": 368, + "versionNonce": 1812882025, + "isDeleted": false, + "boundElements": [], + "updated": 1761730932603, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -5.332155922949113, + 144.35639615162688 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "U3DFf7U24sN5v5OiE6Emp", + "mode": "orbit", + "fixedPoint": [ + 0.4700089000963089, + 0.5299910999036935 + ] + }, + "endBinding": { + "elementId": "z__k3NCHvk401op7pyDyK", + "mode": "orbit", + "fixedPoint": [ + 0.350994946192683, + 0.35099494619268623 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "XKPgnbAxa6f_wEoGW2n-y", + "type": "rectangle", + "x": 208.64997453459932, + "y": 793.4187356095455, + "width": 262.620676705503, + "height": 70.471968713362, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2S", + "roundness": { + "type": 3 + }, + "seed": 388895495, + "version": 224, + "versionNonce": 2139090857, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "x77YtTBk36bTXs49gtdp4" + }, + { + "id": "37HWLa2RtB2HAhUTT_kCW", + "type": "arrow" + }, + { + "id": "Ftv81G0u9I6JpAAOo0BwB", + "type": "arrow" + } + ], + "updated": 1761730947144, + "link": null, + "locked": false + }, + { + "id": "x77YtTBk36bTXs49gtdp4", + "type": "text", + "x": 267.5123758756321, + "y": 818.6547199662265, + "width": 144.8958740234375, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2T", + "roundness": null, + "seed": 819547111, + "version": 184, + "versionNonce": 449303175, + "isDeleted": false, + "boundElements": [], + "updated": 1761730944171, + "link": null, + "locked": false, + "text": "recognise speakers", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "XKPgnbAxa6f_wEoGW2n-y", + "originalText": "recognise speakers", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "37HWLa2RtB2HAhUTT_kCW", + "type": "arrow", + "x": 49.02989574564721, + "y": 850.09887000446, + "width": 160.09491793495448, + "height": 5.448810382554257, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2U", + "roundness": { + "type": 2 + }, + "seed": 2064836551, + "version": 211, + "versionNonce": 324659623, + "isDeleted": false, + "boundElements": [], + "updated": 1761730944172, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 160.09491793495448, + 5.448810382554257 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "z__k3NCHvk401op7pyDyK", + "mode": "orbit", + "fixedPoint": [ + 0.9028442481142557, + 0.09715575188574803 + ] + }, + "endBinding": { + "elementId": "XKPgnbAxa6f_wEoGW2n-y", + "mode": "orbit", + "fixedPoint": [ + 0.10526584528481202, + 0.8947341547151896 + ] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff", + "lockedMultiSelections": {} + }, + "files": {} +} \ No newline at end of file From 0d5546b3d905ad245d3a6f4b03fbd88f8cc08e2c Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Tue, 9 Dec 2025 22:26:44 +0000 Subject: [PATCH 20/21] added script to make mycelia key --- .../scripts/create_mycelia_api_key.py | 215 +++++++++++++++++- 1 file changed, 207 insertions(+), 8 deletions(-) diff --git a/backends/advanced/scripts/create_mycelia_api_key.py b/backends/advanced/scripts/create_mycelia_api_key.py index ac2149e8..42d10bdb 100755 --- a/backends/advanced/scripts/create_mycelia_api_key.py +++ b/backends/advanced/scripts/create_mycelia_api_key.py @@ -10,11 +10,14 @@ from datetime import datetime # MongoDB configuration -MONGO_URL = os.getenv("MONGO_URL", "mongodb://localhost:27018") -MYCELIA_DB = os.getenv("MYCELIA_DB", os.getenv("DATABASE_NAME", "mycelia_test")) +# When running with Docker Compose, MongoDB is on localhost:27017 +# The script auto-detects the environment from backends/advanced/.env if available +MONGO_URL = os.getenv("MONGO_URL", os.getenv("MONGODB_URI", "mongodb://localhost:27017")) +MYCELIA_DB = os.getenv("MYCELIA_DB", os.getenv("DATABASE_NAME", "mycelia")) -# User ID from JWT or argument -USER_ID = os.getenv("USER_ID", "692c7727c7b16bdf58d23cd1") # test user +# User ID from JWT or argument (can be passed via command line or environment) +# This will be determined in main() after loading .env file +USER_ID = None def hash_api_key_with_salt(api_key: str, salt: bytes) -> str: @@ -27,10 +30,202 @@ def hash_api_key_with_salt(api_key: str, salt: bytes) -> str: return base64.b64encode(h.digest()).decode('utf-8') # Use base64 like Mycelia +def load_env_from_file(): + """Load environment variables from environment file.""" + # Check if ENV_NAME is provided (from Makefile) + env_name = os.getenv("ENV_NAME") + + if env_name: + # Load from environments/.env in project root + # Script is at: backends/advanced/scripts/create_mycelia_api_key.py + # Project root is: ../../.. from script location + script_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.join(script_dir, "..", "..", "..") + env_file = os.path.join(project_root, "environments", f"{env_name}.env") + if os.path.exists(env_file): + print(f"๐Ÿ“„ Loading environment: {env_name}\n") + with open(env_file, 'r') as f: + for line in f: + line = line.strip() + # Skip comments and empty lines + if not line or line.startswith('#'): + continue + # Parse KEY=VALUE + if '=' in line: + key, value = line.split('=', 1) + # Remove quotes if present + value = value.strip('"').strip("'") + # Always override critical database variables from environment file + if key in ('MONGODB_DATABASE', 'MONGODB_URI', 'MONGO_URL', 'MYCELIA_DB'): + os.environ[key] = value + # Only set if not already in environment for other variables + elif key not in os.environ: + os.environ[key] = value + + # Also load from generated backends/advanced/.env. to get calculated ports + generated_env = os.path.join(script_dir, "..", f".env.{env_name}") + if os.path.exists(generated_env): + with open(generated_env, 'r') as f: + for line in f: + line = line.strip() + # Skip comments and empty lines + if not line or line.startswith('#'): + continue + # Parse KEY=VALUE + if '=' in line: + key, value = line.split('=', 1) + # Remove quotes if present + value = value.strip('"').strip("'") + # Load port variables from generated file + if 'PORT' in key and key not in os.environ: + os.environ[key] = value + return + else: + print(f"โš ๏ธ Environment file not found: {env_file}\n") + + # Fallback: try to load from backends/advanced/.env (symlink) + env_file = os.path.join(os.path.dirname(__file__), "..", ".env") + if os.path.exists(env_file): + print(f"๐Ÿ“„ Loading environment from: {env_file}\n") + with open(env_file, 'r') as f: + for line in f: + line = line.strip() + # Skip comments and empty lines + if not line or line.startswith('#'): + continue + # Parse KEY=VALUE + if '=' in line: + key, value = line.split('=', 1) + # Remove quotes if present + value = value.strip('"').strip("'") + # Always override critical database variables from environment file + if key in ('MONGODB_DATABASE', 'MONGODB_URI', 'MONGO_URL', 'MYCELIA_DB'): + os.environ[key] = value + # Only set if not already in environment for other variables + elif key not in os.environ: + os.environ[key] = value + else: + print(f"โ„น๏ธ No environment file found") + print(" Run from Makefile: make mycelia-create-token") + print(" Or start environment: ./start-env.sh \n") + + def main(): + # Try to load environment from backends/advanced/.env + load_env_from_file() + + # Re-read configuration after loading .env file + global MONGO_URL, MYCELIA_DB, USER_ID + + # Try to get MongoDB URL from environment + MONGO_URL = os.getenv("MONGO_URL") or os.getenv("MONGODB_URI") + + # If not set, construct from MONGODB_DATABASE or use default + if not MONGO_URL: + mongodb_database = os.getenv("MONGODB_DATABASE", "friend-lite") + MONGO_URL = f"mongodb://localhost:27017/{mongodb_database}" + + # Replace Docker/K8s hostnames with "localhost" when running from host + if "mongo:27017" in MONGO_URL: + MONGO_URL = MONGO_URL.replace("mongo:27017", "localhost:27017") + print(f"โ„น๏ธ Converted Docker MongoDB URL to localhost\n") + elif "mongodb.root.svc.cluster.local" in MONGO_URL: + # Kubernetes service hostname - replace with localhost + MONGO_URL = MONGO_URL.replace("mongodb.root.svc.cluster.local:27017", "localhost:27017") + print(f"โ„น๏ธ Converted Kubernetes MongoDB URL to localhost\n") + elif "mongodb://" in MONGO_URL and "localhost" not in MONGO_URL and "127.0.0.1" not in MONGO_URL: + # Any other remote MongoDB URL - try to use localhost + import re + # Extract database name if present + db_match = re.search(r'mongodb://[^/]+/([^?]+)', MONGO_URL) + if db_match: + db_name = db_match.group(1) + MONGO_URL = f"mongodb://localhost:27017/{db_name}" + else: + MONGO_URL = "mongodb://localhost:27017" + print(f"โ„น๏ธ Using localhost MongoDB instead of remote URL\n") + + MYCELIA_DB = os.getenv("MYCELIA_DB", os.getenv("DATABASE_NAME", "mycelia")) + + # Determine USER_ID from command line arg, environment, or prompt + USER_ID = os.getenv("USER_ID", None) + if USER_ID is None and len(sys.argv) > 1: + USER_ID = sys.argv[1] + + # If still no USER_ID, list available users and prompt + if USER_ID is None: + print("๐Ÿ“‹ Available users in Friend-Lite:") + # Try to list users from the friend-lite database + try: + # Extract base URL without database name + base_url = MONGO_URL.rsplit('/', 1)[0] if '/' in MONGO_URL else MONGO_URL + + # Get the Friend-Lite database name from environment + # MONGODB_DATABASE is set in the environment file (e.g., "friend-lite-test2") + friend_db = os.getenv("MONGODB_DATABASE", "friend-lite") + + print(f" Database: {friend_db}") + print() + + client = MongoClient(base_url, serverSelectionTimeoutMS=5000) + db = client[friend_db] + users = db["users"].find({}, {"_id": 1, "email": 1}) + user_list = list(users) + + if not user_list: + print(" (No users found - create a user in Friend-Lite first)") + client.close() + return 1 + + # Display users with numbers + print() + for idx, user in enumerate(user_list, 1): + print(f" {idx}) {user['email']} (ID: {user['_id']})") + print() + + # Prompt for selection + while True: + try: + selection = input("Select user (number or enter USER_ID): ").strip() + + # Try to parse as number (user selection) + try: + user_idx = int(selection) - 1 + if 0 <= user_idx < len(user_list): + USER_ID = str(user_list[user_idx]['_id']) + print(f"โœ… Selected: {user_list[user_idx]['email']}") + break + else: + print(f"โŒ Invalid selection. Choose 1-{len(user_list)}") + except ValueError: + # Not a number, treat as USER_ID + if len(selection) == 24: # MongoDB ObjectId length + USER_ID = selection + print(f"โœ… Using USER_ID: {USER_ID}") + break + else: + print("โŒ Invalid USER_ID format (should be 24 characters)") + except KeyboardInterrupt: + print("\n\nโŒ Cancelled by user") + client.close() + return 1 + + client.close() + print() + except Exception as e: + print(f" โŒ Could not list users: {e}") + print() + return 1 + + # Get Friend-Lite database name + friend_db = os.getenv("MONGODB_DATABASE", "friend-lite") + print(f"๐Ÿ“Š MongoDB Configuration:") print(f" URL: {MONGO_URL}") - print(f" Database: {MYCELIA_DB}\n") + print(f" Friend-Lite DB: {friend_db}") + print(f" Mycelia DB: {MYCELIA_DB}") + print(f" User ID: {USER_ID}") + print() print("๐Ÿ” Creating Mycelia API Key\n") @@ -90,15 +285,19 @@ def main(): result = api_keys.insert_one(api_key_doc) client_id = str(result.inserted_id) + # Detect Mycelia ports from environment + mycelia_frontend_port = os.getenv("MYCELIA_FRONTEND_PORT", "3002") + mycelia_backend_port = os.getenv("MYCELIA_BACKEND_PORT", os.getenv("MYCELIA_PORT", "5100")) + print(f"๐ŸŽ‰ API Key Created Successfully!") print(f" Client ID: {client_id}") print(f" API Key: {api_key}") print(f"\n" + "=" * 70) - print("๐Ÿ“‹ MYCELIA CONFIGURATION (Test Environment)") + print("๐Ÿ“‹ MYCELIA CONFIGURATION") print("=" * 70) print(f"\n1๏ธโƒฃ Configure Mycelia Frontend Settings:") - print(f" โ€ข Go to: http://localhost:3002/settings") - print(f" โ€ข API Endpoint: http://localhost:5100") + print(f" โ€ข Go to: http://localhost:{mycelia_frontend_port}/settings") + print(f" โ€ข API Endpoint: http://localhost:{mycelia_backend_port}") print(f" โ€ข Client ID: {client_id}") print(f" โ€ข Client Secret: {api_key}") print(f" โ€ข Click 'Save' and then 'Test Token'") From 599603ee2c9221c6c195669720e7535c9f55256d Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Tue, 9 Dec 2025 22:27:02 +0000 Subject: [PATCH 21/21] add mycelia submodule --- extras/mycelia | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/mycelia b/extras/mycelia index 47ea1966..12ec9d7b 160000 --- a/extras/mycelia +++ b/extras/mycelia @@ -1 +1 @@ -Subproject commit 47ea1966dd8a8c10662c91c7a3f907798f6a7dbc +Subproject commit 12ec9d7bbaec23c56555b6e95b2fa102215c0347