- {json}"
+ m = re.search(r"Error code:\s*(\d+)\s*-\s*(\{.*\})", raw_msg, re.DOTALL)
+ if m:
+ error_code = m.group(1)
+ try:
+ parsed = json.loads(m.group(2))
+ err = parsed.get("error") or parsed
+ if isinstance(err, dict):
+ error_obj = {
+ "message": err.get("message"),
+ "type": err.get("type"),
+ "param": err.get("param"),
+ "code": err.get("code"),
+ }
+ if err.get("message"):
+ message = err.get("message")
+ if err.get("code"):
+ error_code = err.get("code")
+ except Exception:
+ pass
+
+ # Heuristics if not parsed
+ if error_obj is None:
+ lower = raw_msg.lower()
+ if "invalid_api_key" in lower or "incorrect api key" in lower or "unauthorized" in lower or " 401" in lower:
+ error_code = "invalid_api_key"
+ message = "Invalid key. Validation failed."
+ error_obj = {
+ "message": message,
+ "type": "invalid_request_error",
+ "param": None,
+ "code": "invalid_api_key",
+ }
+ elif "model_not_found" in lower or "does not exist" in lower or " 404" in lower:
+ error_code = "model_not_found"
+ message = "Invalid model name. Validation failed."
+ error_obj = {
+ "message": message,
+ "type": "invalid_request_error",
+ "param": None,
+ "code": "model_not_found",
+ }
+ elif "insufficient_quota" in lower or "quota" in lower or " 429" in lower:
+ error_code = "insufficient_quota"
+ message = "You exceeded your current quota, please check your plan and billing details."
+ error_obj = {
+ "message": message,
+ "type": "insufficient_quota",
+ "param": None,
+ "code": "insufficient_quota",
+ }
+
+ return message, error_code, error_obj
diff --git a/backend/app/controller/model_controller.py b/backend/app/controller/model_controller.py
index 641f73bd5..fddb30bb0 100644
--- a/backend/app/controller/model_controller.py
+++ b/backend/app/controller/model_controller.py
@@ -1,6 +1,8 @@
from fastapi import APIRouter
from pydantic import BaseModel, Field
from app.component.model_validation import create_agent
+from camel.types import ModelType
+from app.component.error_format import normalize_error_to_openai_format
router = APIRouter(tags=["model"])
@@ -18,12 +20,29 @@ class ValidateModelRequest(BaseModel):
class ValidateModelResponse(BaseModel):
is_valid: bool = Field(..., description="Is valid")
is_tool_calls: bool = Field(..., description="Is tool call used")
+ error_code: str | None = Field(None, description="Error code")
+ error: dict | None = Field(None, description="OpenAI-style error object")
message: str = Field(..., description="Message")
@router.post("/model/validate")
async def validate_model(request: ValidateModelRequest):
try:
+ # API key validation
+ if request.api_key is not None and str(request.api_key).strip() == "":
+ return ValidateModelResponse(
+ is_valid=False,
+ is_tool_calls=False,
+ message="Invalid key. Validation failed.",
+ error_code="invalid_api_key",
+ error={
+ "message": "Invalid key. Validation failed.",
+ "type": "invalid_request_error",
+ "param": None,
+ "code": "invalid_api_key",
+ },
+ )
+
extra = request.extra_params or {}
agent = create_agent(
@@ -43,17 +62,33 @@ async def validate_model(request: ValidateModelRequest):
"""
)
except Exception as e:
- return ValidateModelResponse(is_valid=False, is_tool_calls=False, message=str(e))
+ # Normalize error to OpenAI-style error structure
+ message, error_code, error_obj = normalize_error_to_openai_format(e)
+
+ return ValidateModelResponse(
+ is_valid=False,
+ is_tool_calls=False,
+ message=message,
+ error_code=error_code,
+ error=error_obj,
+ )
is_valid = bool(response)
is_tool_calls = False
-
- if response and hasattr(response, 'info') and response.info:
+
+ if response and hasattr(response, "info") and response.info:
tool_calls = response.info.get("tool_calls", [])
if tool_calls and len(tool_calls) > 0:
- is_tool_calls = tool_calls[0].result == "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
-
+ is_tool_calls = (
+ tool_calls[0].result
+ == "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
+ )
+
return ValidateModelResponse(
is_valid=is_valid,
is_tool_calls=is_tool_calls,
- message="",
+ message="Validation Success"
+ if is_tool_calls
+ else "This model doesn't support tool calls. please try with another model.",
+ error_code=None,
+ error=None,
)
diff --git a/backend/app/controller/tool_controller.py b/backend/app/controller/tool_controller.py
index 88474a2d8..4a18857fe 100644
--- a/backend/app/controller/tool_controller.py
+++ b/backend/app/controller/tool_controller.py
@@ -1,6 +1,7 @@
-from fastapi import APIRouter
-
+from fastapi import APIRouter, HTTPException
+from loguru import logger
from app.utils.toolkit.notion_mcp_toolkit import NotionMCPToolkit
+from app.utils.toolkit.google_calendar_toolkit import GoogleCalendarToolkit
router = APIRouter(tags=["task"])
@@ -8,11 +9,107 @@ router = APIRouter(tags=["task"])
@router.post("/install/tool/{tool}", name="install tool")
async def install_tool(tool: str):
+ """
+ Install and pre-instantiate a specific MCP tool for authentication
+
+ Args:
+ tool: Tool name to install (notion)
+
+ Returns:
+ Installation result with tool information
+ """
if tool == "notion":
- toolkit = NotionMCPToolkit(tool)
- await toolkit.connect()
+ try:
+ # Use a dummy task_id for installation, as this is just for pre-authentication
+ toolkit = NotionMCPToolkit("install_auth")
+
+ try:
+ # Pre-instantiate by connecting (this completes authentication)
+ await toolkit.connect()
+
+ # Get available tools to verify connection
+ tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()]
+ logger.info(f"Successfully pre-instantiated {tool} toolkit with {len(tools)} tools")
+
+ # Disconnect, authentication info is saved
+ await toolkit.disconnect()
+
+ return {
+ "success": True,
+ "tools": tools,
+ "message": f"Successfully installed and authenticated {tool} toolkit",
+ "count": len(tools),
+ "toolkit_name": "NotionMCPToolkit"
+ }
+ except Exception as connect_error:
+ logger.warning(f"Could not connect to {tool} MCP server: {connect_error}")
+ # Even if connection fails, mark as installed so user can use it later
+ return {
+ "success": True,
+ "tools": [],
+ "message": f"{tool} toolkit installed but not connected. Will connect when needed.",
+ "count": 0,
+ "toolkit_name": "NotionMCPToolkit",
+ "warning": "Could not connect to Notion MCP server. You may need to authenticate when using the tool."
+ }
+ except Exception as e:
+ logger.error(f"Failed to install {tool} toolkit: {e}")
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to install {tool}: {str(e)}"
+ )
+ elif tool == "google_calendar":
+ try:
+ # Use a dummy task_id for installation, as this is just for pre-authentication
+ toolkit = GoogleCalendarToolkit("install_auth")
+
+ # Get available tools to verify connection
+ tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()]
+ logger.info(f"Successfully pre-instantiated {tool} toolkit with {len(tools)} tools")
+
+ return {
+ "success": True,
+ "tools": tools,
+ "message": f"Successfully installed {tool} toolkit",
+ "count": len(tools),
+ "toolkit_name": "GoogleCalendarToolkit"
+ }
+ except Exception as e:
+ logger.error(f"Failed to install {tool} toolkit: {e}")
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to install {tool}: {str(e)}"
+ )
else:
- return {"error": "Tool not found"}
- tools = [tool.func.__name__ for tool in toolkit.get_tools()]
- await toolkit.disconnect()
- return tools
+ raise HTTPException(
+ status_code=404,
+ detail=f"Tool '{tool}' not found. Available tools: ['notion', 'google_calendar']"
+ )
+
+
+@router.get("/tools/available", name="list available tools")
+async def list_available_tools():
+ """
+ List all available MCP tools that can be installed
+
+ Returns:
+ List of available tools with their information
+ """
+ return {
+ "tools": [
+ {
+ "name": "notion",
+ "display_name": "Notion MCP",
+ "description": "Notion workspace integration for reading and managing Notion pages",
+ "toolkit_class": "NotionMCPToolkit",
+ "requires_auth": True
+ },
+ {
+ "name": "google_calendar",
+ "display_name": "Google Calendar",
+ "description": "Google Calendar integration for managing events and schedules",
+ "toolkit_class": "GoogleCalendarToolkit",
+ "requires_auth": True
+ }
+ ]
+ }
diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py
index 9013bdfb5..57166b907 100644
--- a/backend/app/utils/agent.py
+++ b/backend/app/utils/agent.py
@@ -725,7 +725,7 @@ def search_agent(options: Chat):
"browser_enter",
"browser_visit_page",
"browser_scroll",
- # "browser_get_som_screenshot",
+ "browser_get_som_screenshot",
],
)
@@ -1441,7 +1441,7 @@ async def get_toolkits(tools: list[str], agent_name: str, api_task_id: str):
"image_analysis_toolkit": ImageAnalysisToolkit,
"linkedin_toolkit": LinkedInToolkit,
"mcp_search_toolkit": McpSearchToolkit,
- "notion_toolkit": NotionMCPToolkit,
+ "notion_mcp_toolkit": NotionMCPToolkit,
"pptx_toolkit": PPTXToolkit,
"reddit_toolkit": RedditToolkit,
"search_toolkit": SearchToolkit,
@@ -1457,9 +1457,9 @@ async def get_toolkits(tools: list[str], agent_name: str, api_task_id: str):
if item in toolkits:
toolkit: AbstractToolkit = toolkits[item]
toolkit.agent_name = agent_name
- res = toolkit.get_can_use_tools(api_task_id)
- res = await res if asyncio.iscoroutine(res) else res
- res.extend(res)
+ toolkit_tools = toolkit.get_can_use_tools(api_task_id)
+ toolkit_tools = await toolkit_tools if asyncio.iscoroutine(toolkit_tools) else toolkit_tools
+ res.extend(toolkit_tools)
else:
logger.warning(f"Toolkit {item} not found, please check your configuration.")
traceroot_logger.warning(f"Toolkit {item} not found for agent {agent_name}")
diff --git a/backend/app/utils/listen/toolkit_listen.py b/backend/app/utils/listen/toolkit_listen.py
index 1f4053fe7..77079c7c5 100644
--- a/backend/app/utils/listen/toolkit_listen.py
+++ b/backend/app/utils/listen/toolkit_listen.py
@@ -24,7 +24,7 @@ def listen_toolkit(
if iscoroutinefunction(func):
# async function wrapper
- @wraps(func)
+ @wraps(wrap)
async def async_wrapper(*args, **kwargs):
toolkit: AbstractToolkit = args[0]
task_lock = get_task_lock(toolkit.api_task_id)
@@ -93,7 +93,7 @@ def listen_toolkit(
else:
# sync function wrapper
- @wraps(func)
+ @wraps(wrap)
def sync_wrapper(*args, **kwargs):
toolkit: AbstractToolkit = args[0]
task_lock = get_task_lock(toolkit.api_task_id)
diff --git a/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py
index 43f889067..911a6dd87 100644
--- a/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py
+++ b/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py
@@ -326,9 +326,9 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
async def browser_get_page_snapshot(self) -> str:
return await super().browser_get_page_snapshot()
- # @listen_toolkit(BaseHybridBrowserToolkit.browser_get_som_screenshot)
- # async def browser_get_som_screenshot(self):
- # return await super().browser_get_som_screenshot()
+ @listen_toolkit(BaseHybridBrowserToolkit.browser_get_som_screenshot)
+ async def browser_get_som_screenshot(self):
+ return await super().browser_get_som_screenshot()
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_page_links)
async def browser_get_page_links(self, *, ref: List[str]) -> Dict[str, Any]:
@@ -362,7 +362,7 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
FunctionTool(browser.browser_enter),
FunctionTool(browser.browser_visit_page),
FunctionTool(browser.browser_scroll),
- # FunctionTool(browser.browser_get_som_screenshot),
+ FunctionTool(browser.browser_get_som_screenshot),
# FunctionTool(browser.select),
# FunctionTool(browser.wait_user),
]
diff --git a/backend/app/utils/toolkit/hybrid_browser_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_toolkit.py
index 07115020d..adf6c3d0c 100644
--- a/backend/app/utils/toolkit/hybrid_browser_toolkit.py
+++ b/backend/app/utils/toolkit/hybrid_browser_toolkit.py
@@ -78,102 +78,9 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper):
self.websocket = None
async def start(self):
- # Check if node_modules exists (dependencies installed)
- node_modules_path = os.path.join(self.ts_dir, "node_modules")
- if not os.path.exists(node_modules_path):
- logger.warning("Node modules not found. Running npm install...")
- install_result = subprocess.run(
- [uv(), "run", "npm", "install"],
- cwd=self.ts_dir,
- capture_output=True,
- text=True,
- )
- if install_result.returncode != 0:
- logger.error(f"npm install failed: {install_result.stderr}")
- raise RuntimeError(
- f"Failed to install npm dependencies: {install_result.stderr}\n" # noqa:E501
- f"Please run 'npm install' in {self.ts_dir} manually."
- )
- logger.info("npm dependencies installed successfully")
-
- # Ensure the TypeScript code is built
- build_result = subprocess.run(
- [uv(), "run", "npm", "run", "build"],
- cwd=self.ts_dir,
- capture_output=True,
- text=True,
- )
- if build_result.returncode != 0:
- logger.error(f"TypeScript build failed: {build_result.stderr}")
- raise RuntimeError(f"TypeScript build failed: {build_result.stderr}")
- else:
- # Log warnings but don't fail on them
- if build_result.stderr:
- logger.warning(f"TypeScript build warnings: {build_result.stderr}")
- logger.info("TypeScript build completed successfully")
-
- # Start the WebSocket server
- self.process = subprocess.Popen(
- [uv(), "run", "node", "websocket-server.js"], # bun not support playwright, use uv nodejs-bin
- cwd=self.ts_dir,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True,
- )
-
- # Wait for server to output the port
- server_ready = False
- timeout = 10 # 10 seconds timeout
- start_time = time.time()
-
- while not server_ready and time.time() - start_time < timeout:
- if self.process.poll() is not None:
- # Process died
- stderr = self.process.stderr.read() # type: ignore
- raise RuntimeError(f"WebSocket server failed to start: {stderr}")
-
- try:
- line = self.process.stdout.readline() # type: ignore
- logger.debug(f"WebSocket server output: {line}")
- if line.startswith("SERVER_READY:"):
- self.server_port = int(line.split(":")[1].strip())
- server_ready = True
- logger.info(f"WebSocket server ready on port {self.server_port}")
- except (ValueError, IndexError):
- continue
-
- if not server_ready:
- self.process.kill()
- raise RuntimeError("WebSocket server failed to start within timeout")
-
- # Connect to the WebSocket server
- try:
- self.websocket = await websockets.connect(
- f"ws://localhost:{self.server_port}",
- ping_interval=30,
- ping_timeout=10,
- max_size=50 * 1024 * 1024, # 50MB limit to match server
- )
- logger.info("Connected to WebSocket server")
- except Exception as e:
- self.process.kill()
- raise RuntimeError(f"Failed to connect to WebSocket server: {e}") from e
-
- # Start the background receiver task - THIS WAS MISSING!
- self._receive_task = asyncio.create_task(self._receive_loop())
- logger.debug("Started WebSocket receiver task")
-
- # Initialize the browser toolkit
- logger.debug(f"send init {self.config}")
- try:
- await self._send_command("init", self.config)
- logger.debug("WebSocket server initialized successfully")
- except RuntimeError as e:
- if "Timeout waiting for response to command: init" in str(e):
- logger.warning("Init timeout - continuing anyway (CDP connection may be slow)")
- # Continue without error - the WebSocket server is likely still initializing
- else:
- raise
+ # Simply use the parent implementation which uses system npm/node
+ logger.info("Starting WebSocket server using parent implementation (system npm/node)")
+ await super().start()
async def _send_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""Send a command to the WebSocket server with enhanced error handling."""
@@ -461,9 +368,9 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
async def browser_get_page_snapshot(self) -> str:
return await super().browser_get_page_snapshot()
- # @listen_toolkit(BaseHybridBrowserToolkit.browser_get_som_screenshot)
- # async def browser_get_som_screenshot(self, read_image: bool = False, instruction: str | None = None) -> str:
- # return await super().browser_get_som_screenshot(read_image, instruction)
+ @listen_toolkit(BaseHybridBrowserToolkit.browser_get_som_screenshot)
+ async def browser_get_som_screenshot(self, read_image: bool = False, instruction: str | None = None) -> str:
+ return await super().browser_get_som_screenshot(read_image, instruction)
@listen_toolkit(BaseHybridBrowserToolkit.browser_click)
async def browser_click(self, *, ref: str) -> Dict[str, Any]:
diff --git a/backend/app/utils/toolkit/notion_mcp_toolkit.py b/backend/app/utils/toolkit/notion_mcp_toolkit.py
index 80522109f..36928aa0a 100644
--- a/backend/app/utils/toolkit/notion_mcp_toolkit.py
+++ b/backend/app/utils/toolkit/notion_mcp_toolkit.py
@@ -1,14 +1,13 @@
import os
-from camel.toolkits import FunctionTool, NotionMCPToolkit as BaseNotionMCPToolkit
-from app.component.command import bun
+from typing import Any, Dict, List
+from loguru import logger
+from camel.toolkits import FunctionTool
from app.component.environment import env
-from app.service.task import Agents
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
from camel.toolkits.mcp_toolkit import MCPToolkit
-class NotionMCPToolkit(BaseNotionMCPToolkit, AbstractToolkit):
- agent_name: str = Agents.social_medium_agent
+class NotionMCPToolkit(MCPToolkit, AbstractToolkit):
def __init__(
self,
@@ -18,29 +17,96 @@ class NotionMCPToolkit(BaseNotionMCPToolkit, AbstractToolkit):
self.api_task_id = api_task_id
if timeout is None:
timeout = 120.0
- super().__init__(timeout)
- self._mcp_toolkit = MCPToolkit(
- config_dict={
- "mcpServers": {
- "notionMCP": {
- "command": bun(),
- "args": ["x", "-y", "eigent-mcp-remote@0.1.22", "https://mcp.notion.com/mcp"],
- "env": {
- "MCP_REMOTE_CONFIG_DIR": env("MCP_REMOTE_CONFIG_DIR", os.path.expanduser("~/.mcp-auth")),
- },
- }
+
+ config_dict={
+ "mcpServers": {
+ "notionMCP": {
+ "command": "npx",
+ "args": [
+ "-y",
+ "mcp-remote",
+ "https://mcp.notion.com/mcp",
+ ],
+ "env": {
+ "MCP_REMOTE_CONFIG_DIR": env("MCP_REMOTE_CONFIG_DIR", os.path.expanduser("~/.mcp-auth")),
+ },
}
- },
- timeout=timeout,
- )
+ }
+ }
+ super().__init__(config_dict=config_dict, timeout=timeout)
+
+ def get_tools(self) -> List[FunctionTool]:
+ r"""Returns a list of tools provided by the NotionMCPToolkit.
+
+ Returns:
+ List[FunctionTool]: List of available tools.
+ """
+ all_tools = []
+ for client in self.clients:
+ try:
+ original_build_schema = client._build_tool_schema
+
+ def create_wrapper(orig_func):
+ def wrapper(mcp_tool):
+ return self._build_custom_tool_schema(
+ mcp_tool, orig_func
+ )
+
+ return wrapper
+
+ client._build_tool_schema = create_wrapper( # type: ignore[method-assign]
+ original_build_schema
+ )
+
+ client_tools = client.get_tools()
+ all_tools.extend(client_tools)
+
+ client._build_tool_schema = original_build_schema # type: ignore[method-assign]
+
+ except Exception as e:
+ logger.error(f"Failed to get tools from client: {e}")
+ return all_tools
+
+ def _build_custom_tool_schema(self, mcp_tool, original_build_schema):
+ r"""Build tool schema with custom modifications."""
+ schema = original_build_schema(mcp_tool)
+ self._customize_function_parameters(schema)
+ return schema
+
+ def _customize_function_parameters(self, schema: Dict[str, Any]) -> None:
+ r"""Customize function parameters for specific functions.
+
+ This method allows modifying parameter descriptions or other schema
+ attributes for specific functions.
+ """
+ function_info = schema.get("function", {})
+ function_name = function_info.get("name", "")
+ parameters = function_info.get("parameters", {})
+ properties = parameters.get("properties", {})
+
+ # Modify the notion-create-pages function to make parent optional
+ if function_name == "notion-create-pages":
+ if "parent" in properties:
+ # Update the parent parameter description
+ properties["parent"]["description"] = (
+ "Optional. The parent under which the new pages will be created. "
+ "This can be a page (page_id), a database page (database_id), or "
+ "a data source/collection under a database (data_source_id). "
+ "If omitted, the new pages will be created as private pages at the workspace level. "
+ "Use data_source_id when you have a collection:// URL from the fetch tool."
+ )
@classmethod
async def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]:
tools = []
- if env("MCP_REMOTE_CONFIG_DIR"):
- toolkit = cls(api_task_id)
+ toolkit = cls(api_task_id)
+ try:
await toolkit.connect()
- for item in toolkit.get_tools():
+ # Use subclass implementation that inlines upstream processing
+ all_tools = toolkit.get_tools()
+ for item in all_tools:
setattr(item, "_toolkit_name", cls.__name__)
tools.append(item)
+ except Exception as e:
+ print(f"Warning: Could not connect to Notion MCP server: {e}")
return tools
diff --git a/backend/app/utils/toolkit/terminal_toolkit.py b/backend/app/utils/toolkit/terminal_toolkit.py
index 28480cb57..c4ff77faa 100644
--- a/backend/app/utils/toolkit/terminal_toolkit.py
+++ b/backend/app/utils/toolkit/terminal_toolkit.py
@@ -1,9 +1,7 @@
import asyncio
import os
-from pathlib import Path
-from typing import Any, Dict
from camel.toolkits.terminal_toolkit import TerminalToolkit as BaseTerminalToolkit
-from app.component.command import uv
+from camel.toolkits.terminal_toolkit.terminal_toolkit import _to_plain
from app.component.environment import env
from app.service.task import Action, ActionTerminalData, Agents, get_task_lock
from app.utils.listen.toolkit_listen import listen_toolkit
@@ -19,14 +17,13 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
api_task_id: str,
agent_name: str | None = None,
timeout: float | None = None,
- shell_sessions: Dict[str, Any] | None = None,
working_directory: str | None = None,
- need_terminal: bool = True,
- use_shell_mode: bool = True,
- clone_current_env: bool = False,
+ use_docker_backend: bool = False,
+ docker_container_name: str | None = None,
+ session_logs_dir: str | None = None,
safe_mode: bool = True,
- interactive: bool = False,
- log_dir: str | None = None,
+ allowed_commands: list[str] | None = None,
+ clone_current_env: bool = False,
):
self.api_task_id = api_task_id
if agent_name is not None:
@@ -35,16 +32,26 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
working_directory = env("file_save_path", os.path.expanduser("~/.eigent/terminal/"))
super().__init__(
timeout=timeout,
- shell_sessions=shell_sessions,
working_directory=working_directory,
- need_terminal=False, # Override the code that creates GUI output logs, use queue for SSE output instead
- use_shell_mode=use_shell_mode,
- clone_current_env=clone_current_env,
+ use_docker_backend=use_docker_backend,
+ docker_container_name=docker_container_name,
+ session_logs_dir=session_logs_dir,
safe_mode=safe_mode,
- interactive=interactive,
- log_dir=log_dir,
+ allowed_commands=allowed_commands,
+ clone_current_env=clone_current_env,
)
+ def _write_to_log(self, log_file: str, content: str) -> None:
+ r"""Write content to log file with optional ANSI stripping.
+
+ Args:
+ log_file (str): Path to the log file
+ content (str): Content to write
+ """
+ # Convert ANSI escape sequences to plain text
+ super()._write_to_log(log_file, content)
+ self._update_terminal_output(_to_plain(content))
+
def _update_terminal_output(self, output: str):
task_lock = get_task_lock(self.api_task_id)
# This method will be called during init. At that time, the process_task_id parameter does not exist, so it is set to be empty default
@@ -61,16 +68,12 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
if hasattr(task_lock, "add_background_task"):
task_lock.add_background_task(task)
- def _ensure_uv_available(self) -> bool:
- self.uv_path = uv()
- return True
-
@listen_toolkit(
BaseTerminalToolkit.shell_exec,
- lambda _, id, command: f"id: {id}, command: {command}",
+ lambda _, id, command, block=True: f"id: {id}, command: {command}, block: {block}",
)
- def shell_exec(self, id: str, command: str) -> str:
- return super().shell_exec(id=id, command=command)
+ def shell_exec(self, id: str, command: str, block: bool = True) -> str:
+ return super().shell_exec(id=id, command=command, block=block)
@listen_toolkit(
BaseTerminalToolkit.shell_view,
@@ -81,17 +84,17 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
@listen_toolkit(
BaseTerminalToolkit.shell_wait,
- lambda _, id, seconds: f"id: {id}, seconds: {seconds}",
+ lambda _, id, wait_seconds=None: f"id: {id}, wait_seconds: {wait_seconds}",
)
- def shell_wait(self, id: str, seconds: int | None = None) -> str:
- return super().shell_wait(id=id, seconds=seconds)
+ def shell_wait(self, id: str, wait_seconds: float = 5.0) -> str:
+ return super().shell_wait(id=id, wait_seconds=wait_seconds)
@listen_toolkit(
BaseTerminalToolkit.shell_write_to_process,
- lambda _, id, input, press_enter: f"id: {id}, input: {input}, press_enter: {press_enter}",
+ lambda _, id, command: f"id: {id}, command: {command}",
)
- def shell_write_to_process(self, id: str, input: str, press_enter: bool) -> str:
- return super().shell_write_to_process(id=id, input=input, press_enter=press_enter)
+ def shell_write_to_process(self, id: str, command: str) -> str:
+ return super().shell_write_to_process(id=id, command=command)
@listen_toolkit(
BaseTerminalToolkit.shell_kill_process,
@@ -101,8 +104,8 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
return super().shell_kill_process(id=id)
@listen_toolkit(
- BaseTerminalToolkit.ask_user_for_help,
- lambda _, id: f"id: {id}",
+ BaseTerminalToolkit.shell_ask_user_for_help,
+ lambda _, id, prompt: f"id: {id}, prompt: {prompt}",
)
- def ask_user_for_help(self, id: str) -> str:
- return super().ask_user_for_help(id=id)
+ def shell_ask_user_for_help(self, id: str, prompt: str) -> str:
+ return super().shell_ask_user_for_help(id=id, prompt=prompt)
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 81cb39cd2..05049886c 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -5,7 +5,7 @@ description = "Add your description here"
readme = "README.md"
requires-python = "==3.10.16"
dependencies = [
- "camel-ai[eigent]>=0.2.76a3",
+ "camel-ai[eigent]==0.2.76a13",
"fastapi>=0.115.12",
"fastapi-babel>=1.0.0",
"uvicorn[standard]>=0.34.2",
@@ -17,8 +17,9 @@ dependencies = [
"inflection>=0.5.1",
"aiofiles>=24.1.0",
"openai>=1.99.3,<2",
- "traceroot>=0.0.4a9",
+ "traceroot>=0.0.5a2",
"nodejs-wheel>=22.18.0",
+ "numpy>=1.23.0,<2.0.0",
]
diff --git a/backend/tests/unit/controller/test_model_controller.py b/backend/tests/unit/controller/test_model_controller.py
index 8826f18ce..c83e0cb2e 100644
--- a/backend/tests/unit/controller/test_model_controller.py
+++ b/backend/tests/unit/controller/test_model_controller.py
@@ -8,66 +8,63 @@ from app.controller.model_controller import validate_model, ValidateModelRequest
@pytest.mark.unit
class TestModelController:
"""Test cases for model controller endpoints."""
-
+
@pytest.mark.asyncio
async def test_validate_model_success(self):
"""Test successful model validation."""
request_data = ValidateModelRequest(
- model_platform="OPENAI",
- model_type="GPT_4O_MINI",
+ model_platform="openai",
+ model_type="gpt-4o",
api_key="test_key",
url="https://api.openai.com/v1",
model_config_dict={"temperature": 0.7},
- extra_params={"max_tokens": 1000}
+ extra_params={"max_tokens": 1000},
)
-
+
mock_agent = MagicMock()
mock_response = MagicMock()
tool_call = MagicMock()
- tool_call.result = "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
+ tool_call.result = (
+ "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
+ )
mock_response.info = {"tool_calls": [tool_call]}
mock_agent.step.return_value = mock_response
-
+
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
response = await validate_model(request_data)
-
+
assert isinstance(response, ValidateModelResponse)
assert response.is_valid is True
assert response.is_tool_calls is True
- assert response.message == ""
+ assert response.message == "Validation Success"
+ assert response.error_code is None
+ assert response.error is None
@pytest.mark.asyncio
async def test_validate_model_creation_failure(self):
"""Test model validation when agent creation fails."""
- request_data = ValidateModelRequest(
- model_platform="INVALID",
- model_type="INVALID_MODEL",
- api_key="invalid_key"
- )
-
- with patch("app.controller.model_controller.create_agent", side_effect=Exception("Invalid model configuration")):
+ request_data = ValidateModelRequest(model_platform="INVALID", model_type="INVALID_MODEL", api_key="invalid_key")
+
+ with patch(
+ "app.controller.model_controller.create_agent", side_effect=Exception("Invalid model configuration")
+ ):
response = await validate_model(request_data)
-
assert isinstance(response, ValidateModelResponse)
assert response.is_valid is False
assert response.is_tool_calls is False
- assert "Invalid model configuration" in response.message
+ assert "Invalid model name" in response.message
@pytest.mark.asyncio
async def test_validate_model_step_failure(self):
"""Test model validation when agent step fails."""
- request_data = ValidateModelRequest(
- model_platform="OPENAI",
- model_type="GPT_4O_MINI",
- api_key="test_key"
- )
-
+ request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o", api_key="test_key")
+
mock_agent = MagicMock()
mock_agent.step.side_effect = Exception("API call failed")
-
+
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
response = await validate_model(request_data)
-
+
assert isinstance(response, ValidateModelResponse)
assert response.is_valid is False
assert response.is_tool_calls is False
@@ -76,125 +73,116 @@ class TestModelController:
@pytest.mark.asyncio
async def test_validate_model_tool_calls_false(self):
"""Test model validation when tool calls fail."""
- request_data = ValidateModelRequest(
- model_platform="OPENAI",
- model_type="GPT_4O_MINI",
- api_key="test_key"
- )
-
+ request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o", api_key="test_key")
+
mock_agent = MagicMock()
mock_response = MagicMock()
tool_call = MagicMock()
tool_call.result = "Different response"
mock_response.info = {"tool_calls": [tool_call]}
mock_agent.step.return_value = mock_response
-
+
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
response = await validate_model(request_data)
-
+
assert isinstance(response, ValidateModelResponse)
assert response.is_valid is True
assert response.is_tool_calls is False
- assert response.message == ""
+ assert response.message == "This model doesn't support tool calls. please try with another model."
@pytest.mark.asyncio
async def test_validate_model_with_minimal_parameters(self):
"""Test model validation with minimal parameters."""
request_data = ValidateModelRequest() # Uses default values
-
+
mock_agent = MagicMock()
mock_response = MagicMock()
tool_call = MagicMock()
- tool_call.result = "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
+ tool_call.result = (
+ "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
+ )
mock_response.info = {"tool_calls": [tool_call]}
mock_agent.step.return_value = mock_response
-
+
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
response = await validate_model(request_data)
-
assert isinstance(response, ValidateModelResponse)
- assert response.is_valid is True
- assert response.is_tool_calls is True
+ assert response.is_valid is False
+ assert response.is_tool_calls is False
+ assert response.error_code is not None
+ assert response.error is not None
@pytest.mark.asyncio
async def test_validate_model_no_response(self):
"""Test model validation when no response is returned."""
- request_data = ValidateModelRequest(
- model_platform="OPENAI",
- model_type="GPT_4O_MINI"
- )
-
+ request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o")
+
mock_agent = MagicMock()
mock_agent.step.return_value = None
-
+
# When response is None, should return False
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
result = await validate_model(request_data)
assert result.is_valid is False
assert result.is_tool_calls is False
+ assert result.error_code is None
+ assert result.error is None
@pytest.mark.integration
class TestModelControllerIntegration:
"""Integration tests for model controller."""
-
+
def test_validate_model_endpoint_integration(self, client: TestClient):
"""Test validate model endpoint through FastAPI test client."""
request_data = {
- "model_platform": "OPENAI",
- "model_type": "GPT_4O_MINI",
+ "model_platform": "openai",
+ "model_type": "gpt-4o",
"api_key": "test_key",
"url": "https://api.openai.com/v1",
"model_config_dict": {"temperature": 0.7},
- "extra_params": {"max_tokens": 1000}
+ "extra_params": {"max_tokens": 1000},
}
-
+
mock_agent = MagicMock()
mock_response = MagicMock()
tool_call = MagicMock()
- tool_call.result = "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
+ tool_call.result = (
+ "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
+ )
mock_response.info = {"tool_calls": [tool_call]}
mock_agent.step.return_value = mock_response
-
+
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
response = client.post("/model/validate", json=request_data)
-
+
assert response.status_code == 200
response_data = response.json()
assert response_data["is_valid"] is True
assert response_data["is_tool_calls"] is True
- assert response_data["message"] == ""
+ assert response_data["message"] == "Validation Success"
def test_validate_model_endpoint_error_integration(self, client: TestClient):
"""Test validate model endpoint error handling through FastAPI test client."""
- request_data = {
- "model_platform": "INVALID",
- "model_type": "INVALID_MODEL"
- }
-
+ request_data = {"model_platform": "INVALID", "model_type": "INVALID_MODEL"}
+
with patch("app.controller.model_controller.create_agent", side_effect=Exception("Test error")):
response = client.post("/model/validate", json=request_data)
-
+
assert response.status_code == 200 # Returns 200 with error in response body
response_data = response.json()
assert response_data["is_valid"] is False
assert response_data["is_tool_calls"] is False
- assert "Test error" in response_data["message"]
+ assert "Invalid model name" in response_data["message"]
@pytest.mark.model_backend
class TestModelControllerWithRealModels:
"""Tests that require real model backends (marked for selective running)."""
-
+
@pytest.mark.asyncio
async def test_validate_model_with_real_openai_model(self):
"""Test model validation with real OpenAI model (requires API key)."""
- request_data = ValidateModelRequest(
- model_platform="OPENAI",
- model_type="GPT_4O_MINI",
- api_key=None, # Would need real API key from environment
- )
-
# This test would validate against real OpenAI API
# Marked as model_backend for selective execution
assert True # Placeholder
@@ -210,55 +198,48 @@ class TestModelControllerWithRealModels:
@pytest.mark.unit
class TestModelControllerErrorCases:
"""Test error cases and edge conditions for model controller."""
-
+
@pytest.mark.asyncio
async def test_validate_model_with_invalid_json_config(self):
"""Test model validation with invalid JSON configuration."""
request_data = ValidateModelRequest(
- model_platform="OPENAI",
- model_type="GPT_4O_MINI",
- model_config_dict={"invalid": float('inf')} # Invalid JSON value
+ model_platform="openai",
+ model_type="gpt-4o",
+ model_config_dict={"invalid": float("inf")}, # Invalid JSON value
)
-
+
with patch("app.controller.model_controller.create_agent", side_effect=ValueError("Invalid configuration")):
response = await validate_model(request_data)
-
+
assert response.is_valid is False
assert "Invalid configuration" in response.message
@pytest.mark.asyncio
async def test_validate_model_with_network_error(self):
"""Test model validation with network connectivity issues."""
- request_data = ValidateModelRequest(
- model_platform="OPENAI",
- model_type="GPT_4O_MINI",
- url="https://invalid-url.com"
- )
-
+ request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o", url="https://invalid-url.com")
+
mock_agent = MagicMock()
mock_agent.step.side_effect = ConnectionError("Network unreachable")
-
+
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
response = await validate_model(request_data)
-
+
assert response.is_valid is False
assert "Network unreachable" in response.message
@pytest.mark.asyncio
async def test_validate_model_with_malformed_tool_calls_response(self):
"""Test model validation with malformed tool calls in response."""
- request_data = ValidateModelRequest(
- model_platform="OPENAI",
- model_type="GPT_4O_MINI"
- )
-
+ request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o")
+
mock_agent = MagicMock()
mock_response = MagicMock()
mock_response.info = {
"tool_calls": [] # Empty tool calls
}
mock_agent.step.return_value = mock_response
-
+
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
# Should handle empty tool calls gracefully
result = await validate_model(request_data)
@@ -268,18 +249,53 @@ class TestModelControllerErrorCases:
@pytest.mark.asyncio
async def test_validate_model_with_missing_info_field(self):
"""Test model validation with missing info field in response."""
- request_data = ValidateModelRequest(
- model_platform="OPENAI",
- model_type="GPT_4O_MINI"
- )
-
+ request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o")
+
mock_agent = MagicMock()
mock_response = MagicMock()
mock_response.info = {} # Missing tool_calls
mock_agent.step.return_value = mock_response
-
+
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
# Should handle missing tool_calls key gracefully
result = await validate_model(request_data)
assert result.is_valid is True # Response exists
assert result.is_tool_calls is False # No tool_calls key
+
+ @pytest.mark.asyncio
+ async def test_validate_model_empty_api_key(self):
+ """Test model validation with empty API key."""
+ request_data = ValidateModelRequest(
+ model_platform="openai",
+ model_type="gpt-4o",
+ api_key="", # Empty API key
+ )
+
+ response = await validate_model(request_data)
+
+ assert response.is_valid is False
+ assert response.is_tool_calls is False
+ assert response.message == "Invalid key. Validation failed."
+ assert response.error_code == "invalid_api_key"
+ assert response.error is not None
+ assert response.error["message"] == "Invalid key. Validation failed."
+ assert response.error["type"] == "invalid_request_error"
+ assert response.error["code"] == "invalid_api_key"
+
+ @pytest.mark.asyncio
+ async def test_validate_model_invalid_model_type(self):
+ """Test model validation with invalid model type."""
+ request_data = ValidateModelRequest(
+ model_platform="openai", model_type="INVALID_MODEL_TYPE", api_key="test_key"
+ )
+
+ response = await validate_model(request_data)
+ assert response.is_valid is False
+ assert response.is_tool_calls is False
+ assert response.message == "Invalid model name. Validation failed."
+ assert response.error_code is not None
+ assert "model_not_found" in response.error_code
+ assert response.error is not None
+ assert response.error["message"] == "Invalid model name. Validation failed."
+ assert response.error["type"] == "invalid_request_error"
+ assert response.error["code"] == "model_not_found"
diff --git a/backend/tests/unit/utils/test_agent.py b/backend/tests/unit/utils/test_agent.py
index b1e2089d2..161db996b 100644
--- a/backend/tests/unit/utils/test_agent.py
+++ b/backend/tests/unit/utils/test_agent.py
@@ -672,7 +672,7 @@ class TestAgentFactoryFunctions:
patch('app.utils.agent.TwitterToolkit') as mock_twitter_toolkit, \
patch('app.utils.agent.LinkedInToolkit') as mock_linkedin_toolkit, \
patch('app.utils.agent.RedditToolkit') as mock_reddit_toolkit, \
- patch('app.utils.agent.NotionMCPToolkit') as mock_notion_toolkit, \
+ patch('app.utils.agent.NotionMCPToolkit') as mock_notion_mcp_toolkit, \
patch('app.utils.agent.GoogleGmailMCPToolkit') as mock_gmail_toolkit, \
patch('app.utils.agent.GoogleCalendarToolkit') as mock_calendar_toolkit, \
patch('app.utils.agent.HumanToolkit') as mock_human_toolkit, \
@@ -684,7 +684,7 @@ class TestAgentFactoryFunctions:
mock_twitter_toolkit.get_can_use_tools.return_value = []
mock_linkedin_toolkit.get_can_use_tools.return_value = []
mock_reddit_toolkit.get_can_use_tools.return_value = []
- mock_notion_toolkit.get_can_use_tools = AsyncMock(return_value=[])
+ mock_notion_mcp_toolkit.get_can_use_tools = AsyncMock(return_value=[])
mock_gmail_toolkit.get_can_use_tools = AsyncMock(return_value=[])
mock_calendar_toolkit.get_can_use_tools.return_value = []
mock_human_toolkit.get_can_use_tools.return_value = []
diff --git a/backend/uv.lock b/backend/uv.lock
index 2ed52395d..cfe0b2776 100644
--- a/backend/uv.lock
+++ b/backend/uv.lock
@@ -22,7 +22,7 @@ wheels = [
[[package]]
name = "aiohttp"
-version = "3.12.13"
+version = "3.12.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -34,37 +34,38 @@ dependencies = [
{ name = "propcache" },
{ name = "yarl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/2d/27e4347660723738b01daa3f5769d56170f232bf4695dd4613340da135bb/aiohttp-3.12.13-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5421af8f22a98f640261ee48aae3a37f0c41371e99412d55eaf2f8a46d5dad29", size = 702090, upload-time = "2025-06-14T15:12:58.938Z" },
- { url = "https://files.pythonhosted.org/packages/10/0b/4a8e0468ee8f2b9aff3c05f2c3a6be1dfc40b03f68a91b31041d798a9510/aiohttp-3.12.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fcda86f6cb318ba36ed8f1396a6a4a3fd8f856f84d426584392083d10da4de0", size = 478440, upload-time = "2025-06-14T15:13:02.981Z" },
- { url = "https://files.pythonhosted.org/packages/b9/c8/2086df2f9a842b13feb92d071edf756be89250f404f10966b7bc28317f17/aiohttp-3.12.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cd71c9fb92aceb5a23c4c39d8ecc80389c178eba9feab77f19274843eb9412d", size = 466215, upload-time = "2025-06-14T15:13:04.817Z" },
- { url = "https://files.pythonhosted.org/packages/a7/3d/d23e5bd978bc8012a65853959b13bd3b55c6e5afc172d89c26ad6624c52b/aiohttp-3.12.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34ebf1aca12845066c963016655dac897651e1544f22a34c9b461ac3b4b1d3aa", size = 1648271, upload-time = "2025-06-14T15:13:06.532Z" },
- { url = "https://files.pythonhosted.org/packages/31/31/e00122447bb137591c202786062f26dd383574c9f5157144127077d5733e/aiohttp-3.12.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:893a4639694c5b7edd4bdd8141be296042b6806e27cc1d794e585c43010cc294", size = 1622329, upload-time = "2025-06-14T15:13:08.394Z" },
- { url = "https://files.pythonhosted.org/packages/04/01/caef70be3ac38986969045f21f5fb802ce517b3f371f0615206bf8aa6423/aiohttp-3.12.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:663d8ee3ffb3494502ebcccb49078faddbb84c1d870f9c1dd5a29e85d1f747ce", size = 1694734, upload-time = "2025-06-14T15:13:09.979Z" },
- { url = "https://files.pythonhosted.org/packages/3f/15/328b71fedecf69a9fd2306549b11c8966e420648a3938d75d3ed5bcb47f6/aiohttp-3.12.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0f8f6a85a0006ae2709aa4ce05749ba2cdcb4b43d6c21a16c8517c16593aabe", size = 1737049, upload-time = "2025-06-14T15:13:11.672Z" },
- { url = "https://files.pythonhosted.org/packages/e6/7a/d85866a642158e1147c7da5f93ad66b07e5452a84ec4258e5f06b9071e92/aiohttp-3.12.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1582745eb63df267c92d8b61ca655a0ce62105ef62542c00a74590f306be8cb5", size = 1641715, upload-time = "2025-06-14T15:13:13.548Z" },
- { url = "https://files.pythonhosted.org/packages/14/57/3588800d5d2f5f3e1cb6e7a72747d1abc1e67ba5048e8b845183259c2e9b/aiohttp-3.12.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d59227776ee2aa64226f7e086638baa645f4b044f2947dbf85c76ab11dcba073", size = 1581836, upload-time = "2025-06-14T15:13:15.086Z" },
- { url = "https://files.pythonhosted.org/packages/2f/55/c913332899a916d85781aa74572f60fd98127449b156ad9c19e23135b0e4/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06b07c418bde1c8e737d8fa67741072bd3f5b0fb66cf8c0655172188c17e5fa6", size = 1625685, upload-time = "2025-06-14T15:13:17.163Z" },
- { url = "https://files.pythonhosted.org/packages/4c/34/26cded195f3bff128d6a6d58d7a0be2ae7d001ea029e0fe9008dcdc6a009/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9445c1842680efac0f81d272fd8db7163acfcc2b1436e3f420f4c9a9c5a50795", size = 1636471, upload-time = "2025-06-14T15:13:19.086Z" },
- { url = "https://files.pythonhosted.org/packages/19/21/70629ca006820fccbcec07f3cd5966cbd966e2d853d6da55339af85555b9/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:09c4767af0b0b98c724f5d47f2bf33395c8986995b0a9dab0575ca81a554a8c0", size = 1611923, upload-time = "2025-06-14T15:13:20.997Z" },
- { url = "https://files.pythonhosted.org/packages/31/80/7fa3f3bebf533aa6ae6508b51ac0de9965e88f9654fa679cc1a29d335a79/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f3854fbde7a465318ad8d3fc5bef8f059e6d0a87e71a0d3360bb56c0bf87b18a", size = 1691511, upload-time = "2025-06-14T15:13:22.54Z" },
- { url = "https://files.pythonhosted.org/packages/0f/7a/359974653a3cdd3e9cee8ca10072a662c3c0eb46a359c6a1f667b0296e2f/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2332b4c361c05ecd381edb99e2a33733f3db906739a83a483974b3df70a51b40", size = 1714751, upload-time = "2025-06-14T15:13:24.366Z" },
- { url = "https://files.pythonhosted.org/packages/2d/24/0aa03d522171ce19064347afeefadb008be31ace0bbb7d44ceb055700a14/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1561db63fa1b658cd94325d303933553ea7d89ae09ff21cc3bcd41b8521fbbb6", size = 1643090, upload-time = "2025-06-14T15:13:26.231Z" },
- { url = "https://files.pythonhosted.org/packages/86/2e/7d4b0026a41e4b467e143221c51b279083b7044a4b104054f5c6464082ff/aiohttp-3.12.13-cp310-cp310-win32.whl", hash = "sha256:a0be857f0b35177ba09d7c472825d1b711d11c6d0e8a2052804e3b93166de1ad", size = 427526, upload-time = "2025-06-14T15:13:27.988Z" },
- { url = "https://files.pythonhosted.org/packages/17/de/34d998da1e7f0de86382160d039131e9b0af1962eebfe53dda2b61d250e7/aiohttp-3.12.13-cp310-cp310-win_amd64.whl", hash = "sha256:fcc30ad4fb5cb41a33953292d45f54ef4066746d625992aeac33b8c681173178", size = 450734, upload-time = "2025-06-14T15:13:29.394Z" },
+ { url = "https://files.pythonhosted.org/packages/47/dc/ef9394bde9080128ad401ac7ede185267ed637df03b51f05d14d1c99ad67/aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", size = 703921, upload-time = "2025-07-29T05:49:43.584Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/42/63fccfc3a7ed97eb6e1a71722396f409c46b60a0552d8a56d7aad74e0df5/aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", size = 480288, upload-time = "2025-07-29T05:49:47.851Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/a2/7b8a020549f66ea2a68129db6960a762d2393248f1994499f8ba9728bbed/aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", size = 468063, upload-time = "2025-07-29T05:49:49.789Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/f5/d11e088da9176e2ad8220338ae0000ed5429a15f3c9dfd983f39105399cd/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", size = 1650122, upload-time = "2025-07-29T05:49:51.874Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6b/b60ce2757e2faed3d70ed45dafee48cee7bfb878785a9423f7e883f0639c/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", size = 1624176, upload-time = "2025-07-29T05:49:53.805Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/de/8c9fde2072a1b72c4fadecf4f7d4be7a85b1d9a4ab333d8245694057b4c6/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", size = 1696583, upload-time = "2025-07-29T05:49:55.338Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ad/07f863ca3d895a1ad958a54006c6dafb4f9310f8c2fdb5f961b8529029d3/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", size = 1738896, upload-time = "2025-07-29T05:49:57.045Z" },
+ { url = "https://files.pythonhosted.org/packages/20/43/2bd482ebe2b126533e8755a49b128ec4e58f1a3af56879a3abdb7b42c54f/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", size = 1643561, upload-time = "2025-07-29T05:49:58.762Z" },
+ { url = "https://files.pythonhosted.org/packages/23/40/2fa9f514c4cf4cbae8d7911927f81a1901838baf5e09a8b2c299de1acfe5/aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", size = 1583685, upload-time = "2025-07-29T05:50:00.375Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/c3/94dc7357bc421f4fb978ca72a201a6c604ee90148f1181790c129396ceeb/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d", size = 1627533, upload-time = "2025-07-29T05:50:02.306Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/3f/1f8911fe1844a07001e26593b5c255a685318943864b27b4e0267e840f95/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", size = 1638319, upload-time = "2025-07-29T05:50:04.282Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/46/27bf57a99168c4e145ffee6b63d0458b9c66e58bb70687c23ad3d2f0bd17/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", size = 1613776, upload-time = "2025-07-29T05:50:05.863Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/7e/1d2d9061a574584bb4ad3dbdba0da90a27fdc795bc227def3a46186a8bc1/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", size = 1693359, upload-time = "2025-07-29T05:50:07.563Z" },
+ { url = "https://files.pythonhosted.org/packages/08/98/bee429b52233c4a391980a5b3b196b060872a13eadd41c3a34be9b1469ed/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", size = 1716598, upload-time = "2025-07-29T05:50:09.33Z" },
+ { url = "https://files.pythonhosted.org/packages/57/39/b0314c1ea774df3392751b686104a3938c63ece2b7ce0ba1ed7c0b4a934f/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", size = 1644940, upload-time = "2025-07-29T05:50:11.334Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/83/3dacb8d3f8f512c8ca43e3fa8a68b20583bd25636ffa4e56ee841ffd79ae/aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", size = 429239, upload-time = "2025-07-29T05:50:12.803Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/f9/470b5daba04d558c9673ca2034f28d067f3202a40e17804425f0c331c89f/aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", size = 452297, upload-time = "2025-07-29T05:50:14.266Z" },
]
[[package]]
name = "aiosignal"
-version = "1.3.2"
+version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "frozenlist" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
]
[[package]]
@@ -96,7 +97,7 @@ wheels = [
[[package]]
name = "anyio"
-version = "4.9.0"
+version = "4.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup" },
@@ -104,21 +105,21 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
+ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
]
[[package]]
name = "asgiref"
-version = "3.9.1"
+version = "3.9.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7f/bf/0f3ecda32f1cb3bf1dca480aca08a7a8a3bdc4bed2343a103f30731565c9/asgiref-3.9.2.tar.gz", hash = "sha256:a0249afacb66688ef258ffe503528360443e2b9a8d8c4581b6ebefa58c841ef1", size = 36894, upload-time = "2025-09-23T15:00:55.136Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" },
]
[[package]]
@@ -141,19 +142,17 @@ wheels = [
[[package]]
name = "av"
-version = "14.4.0"
+version = "15.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/86/f6/0b473dab52dfdea05f28f3578b1c56b6c796ce85e76951bab7c4e38d5a74/av-14.4.0.tar.gz", hash = "sha256:3ecbf803a7fdf67229c0edada0830d6bfaea4d10bfb24f0c3f4e607cd1064b42", size = 3892203, upload-time = "2025-05-16T19:13:35.737Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e9/c3/83e6e73d1592bc54436eae0bc61704ae0cff0c3cfbde7b58af9ed67ebb49/av-15.1.0.tar.gz", hash = "sha256:39cda2dc810e11c1938f8cb5759c41d6b630550236b3365790e67a313660ec85", size = 3774192, upload-time = "2025-08-30T04:41:56.076Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/34/0f/cf6b888747cd1e10eafc4a28942e5b666417c03c39853818900bdaa86116/av-14.4.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:10219620699a65b9829cfa08784da2ed38371f1a223ab8f3523f440a24c8381c", size = 19979523, upload-time = "2025-05-16T19:08:59.751Z" },
- { url = "https://files.pythonhosted.org/packages/45/30/8f09ac71ad23344ff247f16a9229b36b1e2a36214fd56ba55df885e9bf85/av-14.4.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:8bac981fde1c05e231df9f73a06ed9febce1f03fb0f1320707ac2861bba2567f", size = 23765838, upload-time = "2025-05-16T19:09:02.362Z" },
- { url = "https://files.pythonhosted.org/packages/a2/57/e0c30ceb1e59e7b2b88c9cd6bf79a0a979128de19a94b300a700d3a7ca52/av-14.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc634ed5bdeb362f0523b73693b079b540418d35d7f3003654f788ae6c317eef", size = 33122039, upload-time = "2025-05-16T19:09:04.729Z" },
- { url = "https://files.pythonhosted.org/packages/c6/a7/9b3064c49f2d2219ee1b895cc77fca18c84d6121b51c8ce6b7f618a2661b/av-14.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23973ed5c5bec9565094d2b3643f10a6996707ddffa5252e112d578ad34aa9ae", size = 31758563, upload-time = "2025-05-16T19:09:07.679Z" },
- { url = "https://files.pythonhosted.org/packages/23/42/0eafe0de75de6a0db71add8e4ea51ebf090482bad3068f4a874c90fbd110/av-14.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0655f7207db6a211d7cedb8ac6a2f7ccc9c4b62290130e393a3fd99425247311", size = 34750358, upload-time = "2025-05-16T19:09:10.932Z" },
- { url = "https://files.pythonhosted.org/packages/75/33/5430ba9ad73036f2d69395d36f3d57b261c51db6f6542bcfc60087640bb7/av-14.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1edaab73319bfefe53ee09c4b1cf7b141ea7e6678a0a1c62f7bac1e2c68ec4e7", size = 35793636, upload-time = "2025-05-16T19:09:13.726Z" },
- { url = "https://files.pythonhosted.org/packages/00/a9/d8c07f0ab69be05a4939719d7a31dc3e9fb112ee8ec6c9411a6c9c085f0a/av-14.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b54838fa17c031ffd780df07b9962fac1be05220f3c28468f7fe49474f1bf8d2", size = 34123666, upload-time = "2025-05-16T19:09:16.968Z" },
- { url = "https://files.pythonhosted.org/packages/48/e1/2f2f607553f2ac6369e5fc814e77b41f9ceb285ce9d8c02c9ee034b8b6db/av-14.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f4b59ac6c563b9b6197299944145958a8ec34710799fd851f1a889b0cbcd1059", size = 36756157, upload-time = "2025-05-16T19:09:21.447Z" },
- { url = "https://files.pythonhosted.org/packages/d7/f0/d653d4eaa7e68732f8c0013aee40f31ff0cd49e90fdec89cca6c193db207/av-14.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:a0192a584fae9f6cedfac03c06d5bf246517cdf00c8779bc33414404796a526e", size = 27931039, upload-time = "2025-05-16T19:09:24.739Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/6a/91e3e68ae0d1b53b480ec69a96f2ae820fb007bc60e6b821741f31c7ba4e/av-15.1.0-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:cf067b66cee2248220b29df33b60eb4840d9e7b9b75545d6b922f9c41d88c4ee", size = 21781685, upload-time = "2025-08-30T04:39:13.118Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/6d/afa951b9cb615c3bc6d95c4eed280c6cefb52c006f4e15e79043626fab39/av-15.1.0-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:26426163d96fc3bde9a015ba4d60da09ef848d9284fe79b4ca5e60965a008fc5", size = 26962481, upload-time = "2025-08-30T04:39:16.875Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/42/0c384884235c42c439cef28cbd129e4624ad60229119bf3c6c6020805119/av-15.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:92f524541ce74b8a12491d8934164a5c57e983da24826547c212f60123de400b", size = 37571839, upload-time = "2025-08-30T04:39:20.325Z" },
+ { url = "https://files.pythonhosted.org/packages/25/c0/5c967b0872fce1add80a8f50fa7ce11e3e3e5257c2b079263570bc854699/av-15.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:659f9d6145fb2c58e8b31907283b6ba876570f5dd6e7e890d74c09614c436c8e", size = 39070227, upload-time = "2025-08-30T04:39:24.079Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/81/e333056d49363c35a74b828ed5f87c96dfbcc1a506b49d79a31ac773b94d/av-15.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:07a8ae30c0cfc3132eff320a6b27d18a5e0dda36effd0ae28892888f4ee14729", size = 39619362, upload-time = "2025-08-30T04:39:27.7Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ae/50cc2af1bf68452cbfec8d1b2554c18f6d167c8ba6d7ad7707797dfd1541/av-15.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e33a76e38f03bb5de026b9f66ccf23dc01ddd2223221096992cb52ac22e62538", size = 40371627, upload-time = "2025-08-30T04:39:31.207Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e6/381edf1779106dd31c9ef1ac9842f643af4465b8a87cbc278d3eaa76229a/av-15.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aa4bf12bdce20edc2a3b13a2776c474c5ab63e1817d53793714504476eeba82e", size = 31340369, upload-time = "2025-08-30T04:39:34.774Z" },
]
[[package]]
@@ -172,21 +171,21 @@ wheels = [
[[package]]
name = "azure-core"
-version = "1.35.0"
+version = "1.35.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
{ name = "six" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ce/89/f53968635b1b2e53e4aad2dd641488929fef4ca9dfb0b97927fa7697ddf3/azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c", size = 339689, upload-time = "2025-07-03T00:55:23.496Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/6b/2653adc0f33adba8f11b1903701e6b1c10d34ce5d8e25dfa13a422f832b0/azure_core-1.35.1.tar.gz", hash = "sha256:435d05d6df0fff2f73fb3c15493bb4721ede14203f1ff1382aa6b6b2bdd7e562", size = 345290, upload-time = "2025-09-11T22:58:04.481Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d4/78/bf94897361fdd650850f0f2e405b2293e2f12808239046232bdedf554301/azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1", size = 210708, upload-time = "2025-07-03T00:55:25.238Z" },
+ { url = "https://files.pythonhosted.org/packages/27/52/805980aa1ba18282077c484dba634ef0ede1e84eec8be9c92b2e162d0ed6/azure_core-1.35.1-py3-none-any.whl", hash = "sha256:12da0c9e08e48e198f9158b56ddbe33b421477e1dc98c2e1c8f9e254d92c468b", size = 211800, upload-time = "2025-09-11T22:58:06.281Z" },
]
[[package]]
name = "azure-identity"
-version = "1.23.1"
+version = "1.25.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-core" },
@@ -195,9 +194,9 @@ dependencies = [
{ name = "msal-extensions" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b5/29/1201ffbb6a57a16524dd91f3e741b4c828a70aaba436578bdcb3fbcb438c/azure_identity-1.23.1.tar.gz", hash = "sha256:226c1ef982a9f8d5dcf6e0f9ed35eaef2a4d971e7dd86317e9b9d52e70a035e4", size = 266185, upload-time = "2025-07-15T19:16:38.077Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/9e/4c9682a286c3c89e437579bd9f64f311020e5125c1321fd3a653166b5716/azure_identity-1.25.0.tar.gz", hash = "sha256:4177df34d684cddc026e6cf684e1abb57767aa9d84e7f2129b080ec45eee7733", size = 278507, upload-time = "2025-09-12T01:30:04.418Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/99/b3/e2d7ab810eb68575a5c7569b03c0228b8f4ce927ffa6211471b526f270c9/azure_identity-1.23.1-py3-none-any.whl", hash = "sha256:7eed28baa0097a47e3fb53bd35a63b769e6b085bb3cb616dfce2b67f28a004a1", size = 186810, upload-time = "2025-07-15T19:16:40.184Z" },
+ { url = "https://files.pythonhosted.org/packages/75/54/81683b6756676a22e037b209695b08008258e603f7e47c56834029c5922a/azure_identity-1.25.0-py3-none-any.whl", hash = "sha256:becaec086bbdf8d1a6aa4fb080c2772a0f824a97d50c29637ec8cc4933f1e82d", size = 190861, upload-time = "2025-09-12T01:30:06.474Z" },
]
[[package]]
@@ -222,6 +221,7 @@ dependencies = [
{ name = "inflection" },
{ name = "loguru" },
{ name = "nodejs-wheel" },
+ { name = "numpy" },
{ name = "openai" },
{ name = "pydantic-i18n" },
{ name = "pydash" },
@@ -240,18 +240,19 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiofiles", specifier = ">=24.1.0" },
- { name = "camel-ai", extras = ["eigent"], specifier = ">=0.2.76a3" },
+ { name = "camel-ai", extras = ["eigent"], specifier = "==0.2.76a13" },
{ name = "fastapi", specifier = ">=0.115.12" },
{ name = "fastapi-babel", specifier = ">=1.0.0" },
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1" },
{ name = "inflection", specifier = ">=0.5.1" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "nodejs-wheel", specifier = ">=22.18.0" },
+ { name = "numpy", specifier = ">=1.23.0,<2.0.0" },
{ name = "openai", specifier = ">=1.99.3,<2" },
{ name = "pydantic-i18n", specifier = ">=0.4.5" },
{ name = "pydash", specifier = ">=8.0.5" },
{ name = "python-dotenv", specifier = ">=1.1.0" },
- { name = "traceroot", specifier = ">=0.0.4a9" },
+ { name = "traceroot", specifier = ">=0.0.5a2" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.2" },
]
@@ -273,43 +274,43 @@ wheels = [
[[package]]
name = "beautifulsoup4"
-version = "4.13.4"
+version = "4.13.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" },
+ { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" },
]
[[package]]
name = "boto3"
-version = "1.40.9"
+version = "1.40.39"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/72/63/b263070ba4a2815de633d71dd4c5c04c9eb7000d33c510036c9557692324/boto3-1.40.9.tar.gz", hash = "sha256:af3f77a548b3dd7db5046609598a28a9ad5d062437b1783da9b526cc67c38b79", size = 111953, upload-time = "2025-08-13T19:20:32.495Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/5b/2b79e27e19b5dc0360e07cb40c6364dd8f7104fe7b4016ae65a527a2535d/boto3-1.40.39.tar.gz", hash = "sha256:27ca06d4d6f838b056b4935c9eceb92c8d125dbe0e895c5583bcf7130627dcd2", size = 111587, upload-time = "2025-09-25T19:20:02.534Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b6/6d/79fad38fcd7e1fc6961061b46cc87706c5c946088bc4620abf0d0aa49420/boto3-1.40.9-py3-none-any.whl", hash = "sha256:516f5e3f7552b2a7ca4d2c89b338fb4684998c676b11b906e2ab694c91716ba6", size = 140061, upload-time = "2025-08-13T19:20:30.652Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/7e/72b4f38c85ea879b27f90ad0d51f26b26e320bbc86b75664c0cf409d3d84/boto3-1.40.39-py3-none-any.whl", hash = "sha256:e2cab5606269fe9f428981892aa592b7e0c087a038774475fa4cd6c8b5fe0a99", size = 139345, upload-time = "2025-09-25T19:20:00.381Z" },
]
[[package]]
name = "botocore"
-version = "1.40.9"
+version = "1.40.39"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ff/f3/7bf4913b4b61416c014cfee38211d071f75894cca37f7234519c4d8676d1/botocore-1.40.9.tar.gz", hash = "sha256:f4a9c6ed08e8637138e1b5534f89d38c02650974b6458a07690493130e295f68", size = 14325768, upload-time = "2025-08-13T19:20:22.393Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/30/44883126961d895ff8b69b8f7d1b2c60e9a348e38d4354ee597b69b8b5f8/botocore-1.40.39.tar.gz", hash = "sha256:c6efc55cac341811ba90c693d20097db6e2ce903451d94496bccd3f672b1709d", size = 14356776, upload-time = "2025-09-25T19:19:49.842Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/02/e9/367e81e114deb92a6e0d5740f0bff4548af710be318af65265b9aad72237/botocore-1.40.9-py3-none-any.whl", hash = "sha256:d4960a39aab9658bcd0272490003001cb4a8d12b89bb297ccef994ee023fb638", size = 13990592, upload-time = "2025-08-13T19:20:16.942Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/57/2400d0cf030650b02a25a2aeb87729e51cb2aa8d97a2b4d9fec05c671f0b/botocore-1.40.39-py3-none-any.whl", hash = "sha256:144e0e887a9fc198c6772f660fc006028bd1a9ce5eea3caddd848db3e421bc79", size = 14025786, upload-time = "2025-09-25T19:19:46.177Z" },
]
[[package]]
@@ -323,7 +324,7 @@ wheels = [
[[package]]
name = "camel-ai"
-version = "0.2.76a3"
+version = "0.2.76a13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama" },
@@ -336,10 +337,11 @@ dependencies = [
{ name = "psutil" },
{ name = "pydantic" },
{ name = "tiktoken" },
+ { name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/30/36/93cf95aef6aaf7beccd12d40300f6b0c3316cea2466b2232d5e2b3cadb40/camel_ai-0.2.76a3.tar.gz", hash = "sha256:65e3308afe10b6411b14a6ca3fe748e685984615af7077708955b5bbf280a382", size = 940901, upload-time = "2025-09-16T09:54:37.553Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f7/7c/0145edf0307e360557917de28691eb0c41b36b017a28c6b67e58a729a6da/camel_ai-0.2.76a13.tar.gz", hash = "sha256:487570c36a39a333ae8000783babd5a82350a829aaa8aa2ae712470b596cafe1", size = 950278, upload-time = "2025-10-06T06:09:46.064Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/46/5c/8618574ad6628019326872a4e0cdeb2275c7f1948b4571194ba04f0e72aa/camel_ai-0.2.76a3-py3-none-any.whl", hash = "sha256:8f226e52debff92be49359f57cd15da5e913055dba453be2a237c587fb274abe", size = 1382011, upload-time = "2025-09-16T09:54:34.798Z" },
+ { url = "https://files.pythonhosted.org/packages/04/46/9886106669491737631178830bce79bd7bf63391db4d2200f645089dd9df/camel_ai-0.2.76a13-py3-none-any.whl", hash = "sha256:b860412e4a5b5fc31b0cc3d4b1eeefcd02382d9a5aced252856a1eff0285a97b", size = 1400549, upload-time = "2025-10-06T06:09:43.291Z" },
]
[package.optional-dependencies]
@@ -350,6 +352,8 @@ eigent = [
{ name = "exa-py" },
{ name = "ffmpeg-python" },
{ name = "google-api-python-client" },
+ { name = "google-auth-httplib2" },
+ { name = "google-auth-oauthlib" },
{ name = "imageio", extra = ["pyav"] },
{ name = "markitdown", extra = ["all"] },
{ name = "mcp-server-fetch" },
@@ -376,34 +380,34 @@ eigent = [
[[package]]
name = "certifi"
-version = "2025.6.15"
+version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
name = "cffi"
-version = "1.17.1"
+version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "pycparser" },
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" },
- { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" },
- { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" },
- { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" },
- { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" },
- { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" },
- { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" },
- { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" },
- { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" },
- { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" },
- { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" },
- { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" },
+ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
+ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
+ { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
+ { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
+ { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
+ { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
+ { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
]
[[package]]
@@ -417,24 +421,22 @@ wheels = [
[[package]]
name = "charset-normalizer"
-version = "3.4.2"
+version = "3.4.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" },
- { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" },
- { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" },
- { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" },
- { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" },
- { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" },
- { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" },
- { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" },
- { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" },
- { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" },
- { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" },
- { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" },
- { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" },
- { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" },
+ { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" },
+ { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" },
+ { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" },
+ { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
]
[[package]]
@@ -481,43 +483,46 @@ wheels = [
[[package]]
name = "cryptography"
-version = "45.0.4"
+version = "46.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198, upload-time = "2025-09-17T00:10:35.797Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cc/1c/92637793de053832523b410dbe016d3f5c11b41d0cf6eef8787aabb51d41/cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069", size = 7055712, upload-time = "2025-06-10T00:02:38.826Z" },
- { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" },
- { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" },
- { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" },
- { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" },
- { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" },
- { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" },
- { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" },
- { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" },
- { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" },
- { url = "https://files.pythonhosted.org/packages/70/d4/994773a261d7ff98034f72c0e8251fe2755eac45e2265db4c866c1c6829c/cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257", size = 2932769, upload-time = "2025-06-10T00:02:58.467Z" },
- { url = "https://files.pythonhosted.org/packages/5a/42/c80bd0b67e9b769b364963b5252b17778a397cefdd36fa9aa4a5f34c599a/cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8", size = 3410441, upload-time = "2025-06-10T00:03:00.14Z" },
- { url = "https://files.pythonhosted.org/packages/ce/0b/2488c89f3a30bc821c9d96eeacfcab6ff3accc08a9601ba03339c0fd05e5/cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723", size = 7031836, upload-time = "2025-06-10T00:03:01.726Z" },
- { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" },
- { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" },
- { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" },
- { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" },
- { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" },
- { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" },
- { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" },
- { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" },
- { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" },
- { url = "https://files.pythonhosted.org/packages/cd/37/1a3cba4c5a468ebf9b95523a5ef5651244693dc712001e276682c278fc00/cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97", size = 2924557, upload-time = "2025-06-10T00:03:22.563Z" },
- { url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508, upload-time = "2025-06-10T00:03:24.586Z" },
- { url = "https://files.pythonhosted.org/packages/16/33/b38e9d372afde56906a23839302c19abdac1c505bfb4776c1e4b07c3e145/cryptography-45.0.4-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a77c6fb8d76e9c9f99f2f3437c1a4ac287b34eaf40997cfab1e9bd2be175ac39", size = 3580103, upload-time = "2025-06-10T00:03:26.207Z" },
- { url = "https://files.pythonhosted.org/packages/c4/b9/357f18064ec09d4807800d05a48f92f3b369056a12f995ff79549fbb31f1/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7aad98a25ed8ac917fdd8a9c1e706e5a0956e06c498be1f713b61734333a4507", size = 4143732, upload-time = "2025-06-10T00:03:27.896Z" },
- { url = "https://files.pythonhosted.org/packages/c4/9c/7f7263b03d5db329093617648b9bd55c953de0b245e64e866e560f9aac07/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3530382a43a0e524bc931f187fc69ef4c42828cf7d7f592f7f249f602b5a4ab0", size = 4385424, upload-time = "2025-06-10T00:03:29.992Z" },
- { url = "https://files.pythonhosted.org/packages/a6/5a/6aa9d8d5073d5acc0e04e95b2860ef2684b2bd2899d8795fc443013e263b/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:6b613164cb8425e2f8db5849ffb84892e523bf6d26deb8f9bb76ae86181fa12b", size = 4142438, upload-time = "2025-06-10T00:03:31.782Z" },
- { url = "https://files.pythonhosted.org/packages/42/1c/71c638420f2cdd96d9c2b287fec515faf48679b33a2b583d0f1eda3a3375/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:96d4819e25bf3b685199b304a0029ce4a3caf98947ce8a066c9137cc78ad2c58", size = 4384622, upload-time = "2025-06-10T00:03:33.491Z" },
- { url = "https://files.pythonhosted.org/packages/ef/ab/e3a055c34e97deadbf0d846e189237d3385dca99e1a7e27384c3b2292041/cryptography-45.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b97737a3ffbea79eebb062eb0d67d72307195035332501722a9ca86bab9e3ab2", size = 3328911, upload-time = "2025-06-10T00:03:35.035Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/8c/44ee01267ec01e26e43ebfdae3f120ec2312aa72fa4c0507ebe41a26739f/cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475", size = 7285044, upload-time = "2025-09-17T00:08:36.807Z" },
+ { url = "https://files.pythonhosted.org/packages/22/59/9ae689a25047e0601adfcb159ec4f83c0b4149fdb5c3030cc94cd218141d/cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080", size = 4308182, upload-time = "2025-09-17T00:08:39.388Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ee/ca6cc9df7118f2fcd142c76b1da0f14340d77518c05b1ebfbbabca6b9e7d/cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e", size = 4572393, upload-time = "2025-09-17T00:08:41.663Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/a3/0f5296f63815d8e985922b05c31f77ce44787b3127a67c0b7f70f115c45f/cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6", size = 4308400, upload-time = "2025-09-17T00:08:43.559Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/8c/74fcda3e4e01be1d32775d5b4dd841acaac3c1b8fa4d0774c7ac8d52463d/cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8", size = 4015786, upload-time = "2025-09-17T00:08:45.758Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b8/85d23287baeef273b0834481a3dd55bbed3a53587e3b8d9f0898235b8f91/cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28", size = 4982606, upload-time = "2025-09-17T00:08:47.602Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d3/de61ad5b52433b389afca0bc70f02a7a1f074651221f599ce368da0fe437/cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9", size = 4604234, upload-time = "2025-09-17T00:08:49.879Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/1f/dbd4d6570d84748439237a7478d124ee0134bf166ad129267b7ed8ea6d22/cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736", size = 4307669, upload-time = "2025-09-17T00:08:52.321Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/fd/ca0a14ce7f0bfe92fa727aacaf2217eb25eb7e4ed513b14d8e03b26e63ed/cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b", size = 4947579, upload-time = "2025-09-17T00:08:54.697Z" },
+ { url = "https://files.pythonhosted.org/packages/89/6b/09c30543bb93401f6f88fce556b3bdbb21e55ae14912c04b7bf355f5f96c/cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab", size = 4603669, upload-time = "2025-09-17T00:08:57.16Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9a/38cb01cb09ce0adceda9fc627c9cf98eb890fc8d50cacbe79b011df20f8a/cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75", size = 4435828, upload-time = "2025-09-17T00:08:59.606Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/53/435b5c36a78d06ae0bef96d666209b0ecd8f8181bfe4dda46536705df59e/cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5", size = 4709553, upload-time = "2025-09-17T00:09:01.832Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/c4/0da6e55595d9b9cd3b6eb5dc22f3a07ded7f116a3ea72629cab595abb804/cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0", size = 3058327, upload-time = "2025-09-17T00:09:03.726Z" },
+ { url = "https://files.pythonhosted.org/packages/95/0f/cd29a35e0d6e78a0ee61793564c8cff0929c38391cb0de27627bdc7525aa/cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7", size = 3523893, upload-time = "2025-09-17T00:09:06.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/dd/eea390f3e78432bc3d2f53952375f8b37cb4d37783e626faa6a51e751719/cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0", size = 2932145, upload-time = "2025-09-17T00:09:08.568Z" },
+ { url = "https://files.pythonhosted.org/packages/98/e5/fbd632385542a3311915976f88e0dfcf09e62a3fc0aff86fb6762162a24d/cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b", size = 7255677, upload-time = "2025-09-17T00:09:42.407Z" },
+ { url = "https://files.pythonhosted.org/packages/56/3e/13ce6eab9ad6eba1b15a7bd476f005a4c1b3f299f4c2f32b22408b0edccf/cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8", size = 4301110, upload-time = "2025-09-17T00:09:45.614Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/67/65dc233c1ddd688073cf7b136b06ff4b84bf517ba5529607c9d79720fc67/cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead", size = 4562369, upload-time = "2025-09-17T00:09:47.601Z" },
+ { url = "https://files.pythonhosted.org/packages/17/db/d64ae4c6f4e98c3dac5bf35dd4d103f4c7c345703e43560113e5e8e31b2b/cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2", size = 4302126, upload-time = "2025-09-17T00:09:49.335Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/19/5f1eea17d4805ebdc2e685b7b02800c4f63f3dd46cfa8d4c18373fea46c8/cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32", size = 4009431, upload-time = "2025-09-17T00:09:51.239Z" },
+ { url = "https://files.pythonhosted.org/packages/81/b5/229ba6088fe7abccbfe4c5edb96c7a5ad547fac5fdd0d40aa6ea540b2985/cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef", size = 4980739, upload-time = "2025-09-17T00:09:54.181Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/9c/50aa38907b201e74bc43c572f9603fa82b58e831bd13c245613a23cff736/cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0", size = 4592289, upload-time = "2025-09-17T00:09:56.731Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/33/229858f8a5bb22f82468bb285e9f4c44a31978d5f5830bb4ea1cf8a4e454/cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128", size = 4301815, upload-time = "2025-09-17T00:09:58.548Z" },
+ { url = "https://files.pythonhosted.org/packages/52/cb/b76b2c87fbd6ed4a231884bea3ce073406ba8e2dae9defad910d33cbf408/cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca", size = 4943251, upload-time = "2025-09-17T00:10:00.475Z" },
+ { url = "https://files.pythonhosted.org/packages/94/0f/f66125ecf88e4cb5b8017ff43f3a87ede2d064cb54a1c5893f9da9d65093/cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc", size = 4591247, upload-time = "2025-09-17T00:10:02.874Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/22/9f3134ae436b63b463cfdf0ff506a0570da6873adb4bf8c19b8a5b4bac64/cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7", size = 4428534, upload-time = "2025-09-17T00:10:04.994Z" },
+ { url = "https://files.pythonhosted.org/packages/89/39/e6042bcb2638650b0005c752c38ea830cbfbcbb1830e4d64d530000aa8dc/cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a", size = 4699541, upload-time = "2025-09-17T00:10:06.925Z" },
+ { url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779, upload-time = "2025-09-17T00:10:08.951Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226, upload-time = "2025-09-17T00:10:10.769Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" },
+ { url = "https://files.pythonhosted.org/packages/14/b9/b260180b31a66859648cfed5c980544ee22b15f8bd20ef82a23f58c0b83e/cryptography-46.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd4b5e2ee4e60425711ec65c33add4e7a626adef79d66f62ba0acfd493af282d", size = 3714683, upload-time = "2025-09-17T00:10:15.601Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/5a/1cd3ef86e5884edcbf8b27c3aa8f9544e9b9fcce5d3ed8b86959741f4f8e/cryptography-46.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48948940d0ae00483e85e9154bb42997d0b77c21e43a77b7773c8c80de532ac5", size = 3443784, upload-time = "2025-09-17T00:10:18.014Z" },
]
[[package]]
@@ -594,11 +599,11 @@ wheels = [
[[package]]
name = "docstring-parser"
-version = "0.15"
+version = "0.17.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e8/2d/ea1dfc15b909cc660f657a3a9d698a2916b7f3b05535a2d72e8d7ea3ad5b/docstring_parser-0.15.tar.gz", hash = "sha256:48ddc093e8b1865899956fcc03b03e66bb7240c310fac5af81814580c55bf682", size = 26768, upload-time = "2022-09-05T07:36:08.139Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/89/e3/32e272db7adcf90e93f73e9a98fd763049ed7c641fb57ab26cb8f3e7e79c/docstring_parser-0.15-py3-none-any.whl", hash = "sha256:d1679b86250d269d06a99670924d6bce45adc00b08069dae8c47d98e89b667a9", size = 36093, upload-time = "2022-09-05T07:36:05.303Z" },
+ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
]
[[package]]
@@ -622,7 +627,7 @@ wheels = [
[[package]]
name = "exa-py"
-version = "1.14.13"
+version = "1.15.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -631,9 +636,9 @@ dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/82/02/6a328cb347c7ec36d84e0f746e1abf5cdc0f09276d14e40d9ad0e2f5efac/exa_py-1.14.13.tar.gz", hash = "sha256:fb86287d12ef44c1386586ef811c35b8818ed008083d4c884ee55964bfd5cbf1", size = 32576, upload-time = "2025-06-27T02:24:19.711Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/91/4c/3eb7c6d80a5b6beb38752210f5c940c70f65464140db33ae261b2a4825bc/exa_py-1.15.6.tar.gz", hash = "sha256:67bb1c0902956b0e23325cc1f9ee990d21277d77b962a40c8902f5eda2407fff", size = 41185, upload-time = "2025-09-10T01:36:01.679Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/25/d1/0ab4e64bd859fdda9b27a97b883688bf969768ccc7d166463cbeeee3a9ed/exa_py-1.14.13-py3-none-any.whl", hash = "sha256:d5d6331181d08bff91205fbaf91db4c52685d7dc66b01377b192c955688cb5c5", size = 42051, upload-time = "2025-06-27T02:24:18.712Z" },
+ { url = "https://files.pythonhosted.org/packages/92/9c/41f032ef35a44262dfe59eeaa4ee6448c9a86fd3b59dbb7448eec9953858/exa_py-1.15.6-py3-none-any.whl", hash = "sha256:8bdbe8d9548408f37b895eed7497046bed3e19a84b5f06bf23a540d4e26b636c", size = 56456, upload-time = "2025-09-10T01:36:00.097Z" },
]
[[package]]
@@ -650,16 +655,16 @@ wheels = [
[[package]]
name = "fastapi"
-version = "0.115.13"
+version = "0.117.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307", size = 295680, upload-time = "2025-06-17T11:49:45.575Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/7e/d9788300deaf416178f61fb3c2ceb16b7d0dc9f82a08fdb87a5e64ee3cc7/fastapi-0.117.1.tar.gz", hash = "sha256:fb2d42082d22b185f904ca0ecad2e195b851030bd6c5e4c032d1c981240c631a", size = 307155, upload-time = "2025-09-20T20:16:56.663Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/45/d9d3e8eeefbe93be1c50060a9d9a9f366dba66f288bb518a9566a23a8631/fastapi-0.117.1-py3-none-any.whl", hash = "sha256:33c51a0d21cab2b9722d4e56dbb9316f3687155be6b276191790d8da03507552", size = 95959, upload-time = "2025-09-20T20:16:53.661Z" },
]
[[package]]
@@ -678,14 +683,14 @@ wheels = [
[[package]]
name = "feedparser"
-version = "6.0.11"
+version = "6.0.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sgmllib3k" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ff/aa/7af346ebeb42a76bf108027fe7f3328bb4e57a3a96e53e21fd9ef9dd6dd0/feedparser-6.0.11.tar.gz", hash = "sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5", size = 286197, upload-time = "2023-12-10T16:03:20.854Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz", hash = "sha256:64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", size = 286579, upload-time = "2025-09-10T13:33:59.486Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/d4/8c31aad9cc18f451c49f7f9cfb5799dadffc88177f7917bc90a66459b1d7/feedparser-6.0.11-py3-none-any.whl", hash = "sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45", size = 81343, upload-time = "2023-12-10T16:03:19.484Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl", hash = "sha256:6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", size = 81480, upload-time = "2025-09-10T13:33:58.022Z" },
]
[[package]]
@@ -702,37 +707,37 @@ wheels = [
[[package]]
name = "filelock"
-version = "3.18.0"
+version = "3.19.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
+ { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
]
[[package]]
name = "flatbuffers"
-version = "25.2.10"
+version = "25.9.23"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170, upload-time = "2025-02-11T04:26:46.257Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9d/1f/3ee70b0a55137442038f2a33469cc5fddd7e0ad2abf83d7497c18a2b6923/flatbuffers-25.9.23.tar.gz", hash = "sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12", size = 22067, upload-time = "2025-09-24T05:25:30.106Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953, upload-time = "2025-02-11T04:26:44.484Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/1b/00a78aa2e8fbd63f9af08c9c19e6deb3d5d66b4dda677a0f61654680ee89/flatbuffers-25.9.23-py2.py3-none-any.whl", hash = "sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2", size = 30869, upload-time = "2025-09-24T05:25:28.912Z" },
]
[[package]]
name = "fonttools"
-version = "4.58.4"
+version = "4.60.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2e/5a/1124b2c8cb3a8015faf552e92714040bcdbc145dfa29928891b02d147a18/fonttools-4.58.4.tar.gz", hash = "sha256:928a8009b9884ed3aae17724b960987575155ca23c6f0b8146e400cc9e0d44ba", size = 3525026, upload-time = "2025-06-13T17:25:15.426Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/27/d9/4eabd956fe123651a1f0efe29d9758b3837b5ae9a98934bdb571117033bb/fonttools-4.60.0.tar.gz", hash = "sha256:8f5927f049091a0ca74d35cce7f78e8f7775c83a6901a8fbe899babcc297146a", size = 3553671, upload-time = "2025-09-17T11:34:01.504Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ed/86/d22c24caa574449b56e994ed1a96d23b23af85557fb62a92df96439d3f6c/fonttools-4.58.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:834542f13fee7625ad753b2db035edb674b07522fcbdd0ed9e9a9e2a1034467f", size = 2748349, upload-time = "2025-06-13T17:23:49.179Z" },
- { url = "https://files.pythonhosted.org/packages/f9/b8/384aca93856def00e7de30341f1e27f439694857d82c35d74a809c705ed0/fonttools-4.58.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2e6c61ce330142525296170cd65666e46121fc0d44383cbbcfa39cf8f58383df", size = 2318565, upload-time = "2025-06-13T17:23:52.144Z" },
- { url = "https://files.pythonhosted.org/packages/1a/f2/273edfdc8d9db89ecfbbf659bd894f7e07b6d53448b19837a4bdba148d17/fonttools-4.58.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9c75f8faa29579c0fbf29b56ae6a3660c6c025f3b671803cb6a9caa7e4e3a98", size = 4838855, upload-time = "2025-06-13T17:23:54.039Z" },
- { url = "https://files.pythonhosted.org/packages/13/fa/403703548c093c30b52ab37e109b369558afa221130e67f06bef7513f28a/fonttools-4.58.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:88dedcedbd5549e35b2ea3db3de02579c27e62e51af56779c021e7b33caadd0e", size = 4767637, upload-time = "2025-06-13T17:23:56.17Z" },
- { url = "https://files.pythonhosted.org/packages/6e/a8/3380e1e0bff6defb0f81c9abf274a5b4a0f30bc8cab4fd4e346c6f923b4c/fonttools-4.58.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae80a895adab43586f4da1521d58fd4f4377cef322ee0cc205abcefa3a5effc3", size = 4819397, upload-time = "2025-06-13T17:23:58.263Z" },
- { url = "https://files.pythonhosted.org/packages/cd/1b/99e47eb17a8ca51d808622a4658584fa8f340857438a4e9d7ac326d4a041/fonttools-4.58.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0d3acc7f0d151da116e87a182aefb569cf0a3c8e0fd4c9cd0a7c1e7d3e7adb26", size = 4926641, upload-time = "2025-06-13T17:24:00.368Z" },
- { url = "https://files.pythonhosted.org/packages/31/75/415254408f038e35b36c8525fc31feb8561f98445688dd2267c23eafd7a2/fonttools-4.58.4-cp310-cp310-win32.whl", hash = "sha256:1244f69686008e7e8d2581d9f37eef330a73fee3843f1107993eb82c9d306577", size = 2201917, upload-time = "2025-06-13T17:24:02.587Z" },
- { url = "https://files.pythonhosted.org/packages/c5/69/f019a15ed2946317c5318e1bcc8876f8a54a313848604ad1d4cfc4c07916/fonttools-4.58.4-cp310-cp310-win_amd64.whl", hash = "sha256:2a66c0af8a01eb2b78645af60f3b787de5fe5eb1fd8348163715b80bdbfbde1f", size = 2246327, upload-time = "2025-06-13T17:24:04.087Z" },
- { url = "https://files.pythonhosted.org/packages/0b/2f/c536b5b9bb3c071e91d536a4d11f969e911dbb6b227939f4c5b0bca090df/fonttools-4.58.4-py3-none-any.whl", hash = "sha256:a10ce13a13f26cbb9f37512a4346bb437ad7e002ff6fa966a7ce7ff5ac3528bd", size = 1114660, upload-time = "2025-06-13T17:25:13.321Z" },
+ { url = "https://files.pythonhosted.org/packages/01/1e/7c2d660cd2a6718961946f76b6af25ae8c7ad0e2a93a34c9bf8b955cb77f/fonttools-4.60.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:151282a235c36024168c21c02193e939e8b28c73d5fa0b36ae1072671d8fa134", size = 2809773, upload-time = "2025-09-17T11:31:52.648Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/74/35cb2e17d984e712f0f7241b1b8bf06bc1b0da345f11620acd78a7eb1f0e/fonttools-4.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3f32cc42d485d9b1546463b9a7a92bdbde8aef90bac3602503e04c2ddb27e164", size = 2345916, upload-time = "2025-09-17T11:31:55.817Z" },
+ { url = "https://files.pythonhosted.org/packages/40/52/39e50212f47bad254255734903accb4f44143faf2b950ba67a61f0bfb26a/fonttools-4.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:336b89d169c40379b8ccef418c877edbc28840b553099c9a739b0db2bcbb57c5", size = 4863583, upload-time = "2025-09-17T11:31:57.708Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/2c/e701ba6a439119fe312f1ad738369519b446503b02d3f0f75424111686f1/fonttools-4.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39a38d950b2b04cd6da729586e6b51d686b0c27d554a2154a6a35887f87c09b1", size = 4793647, upload-time = "2025-09-17T11:31:59.944Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/04/a48f5f7cce1653a876d6b57d9626c1364bcb430780bbbdd475662bbbf759/fonttools-4.60.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7067dd03e0296907a5c6184285807cbb7bc0bf61a584ffebbf97c2b638d8641a", size = 4842891, upload-time = "2025-09-17T11:32:02.149Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/af/0f2b742f6b489a62c6f5a2239867c6d203e3ba358cb48dfc940baee41932/fonttools-4.60.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:342753fe1a1bd2e6896e7a4e936a67c0f441d6897bd11477f718e772d6e63e88", size = 4953569, upload-time = "2025-09-17T11:32:04.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/2b/23c4dde4a869aa138f5fb63fb124e6accb0d643600b437f4eca0f2637ea2/fonttools-4.60.0-cp310-cp310-win32.whl", hash = "sha256:0746c2b2b32087da2ac5f81e14d319c44cb21127d419bc60869daed089790e3d", size = 2231022, upload-time = "2025-09-17T11:32:06.617Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/1c/d53dd15d3392d8f69aa3bc49ca7bdfaea06aa875dc3a641eca85433c90b3/fonttools-4.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:b83b32e5e8918f8e0ccd79816fc2f914e30edc6969ab2df6baf4148e72dbcc11", size = 2275804, upload-time = "2025-09-17T11:32:08.578Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/a4/247d3e54eb5ed59e94e09866cfc4f9567e274fbf310ba390711851f63b3b/fonttools-4.60.0-py3-none-any.whl", hash = "sha256:496d26e4d14dcccdd6ada2e937e4d174d3138e3d73f5c9b6ec6eb2fd1dab4f66", size = 1142186, upload-time = "2025-09-17T11:33:59.287Z" },
]
[[package]]
@@ -843,6 +848,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253, upload-time = "2023-12-12T17:40:13.055Z" },
]
+[[package]]
+name = "google-auth-oauthlib"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "google-auth" },
+ { name = "requests-oauthlib" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/0f/1772edb8d75ecf6280f1c7f51cbcebe274e8b17878b382f63738fd96cee5/google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263", size = 24970, upload-time = "2024-07-08T23:11:24.377Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/8e/22a28dfbd218033e4eeaf3a0533b2b54852b6530da0c0fe934f0cc494b29/google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f", size = 24930, upload-time = "2024-07-08T23:11:23.038Z" },
+]
+
[[package]]
name = "googleapis-common-protos"
version = "1.70.0"
@@ -857,20 +875,23 @@ wheels = [
[[package]]
name = "grpcio"
-version = "1.74.0"
+version = "1.75.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/38/b4/35feb8f7cab7239c5b94bd2db71abb3d6adb5f335ad8f131abb6060840b6/grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1", size = 12756048, upload-time = "2025-07-24T18:54:23.039Z" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/f7/8963848164c7604efb3a3e6ee457fdb3a469653e19002bd24742473254f8/grpcio-1.75.1.tar.gz", hash = "sha256:3e81d89ece99b9ace23a6916880baca613c03a799925afb2857887efa8b1b3d2", size = 12731327, upload-time = "2025-09-26T09:03:36.887Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/66/54/68e51a90797ad7afc5b0a7881426c337f6a9168ebab73c3210b76aa7c90d/grpcio-1.74.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:85bd5cdf4ed7b2d6438871adf6afff9af7096486fcf51818a81b77ef4dd30907", size = 5481935, upload-time = "2025-07-24T18:52:43.756Z" },
- { url = "https://files.pythonhosted.org/packages/32/2a/af817c7e9843929e93e54d09c9aee2555c2e8d81b93102a9426b36e91833/grpcio-1.74.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:68c8ebcca945efff9d86d8d6d7bfb0841cf0071024417e2d7f45c5e46b5b08eb", size = 10986796, upload-time = "2025-07-24T18:52:47.219Z" },
- { url = "https://files.pythonhosted.org/packages/d5/94/d67756638d7bb07750b07d0826c68e414124574b53840ba1ff777abcd388/grpcio-1.74.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:e154d230dc1bbbd78ad2fdc3039fa50ad7ffcf438e4eb2fa30bce223a70c7486", size = 5983663, upload-time = "2025-07-24T18:52:49.463Z" },
- { url = "https://files.pythonhosted.org/packages/35/f5/c5e4853bf42148fea8532d49e919426585b73eafcf379a712934652a8de9/grpcio-1.74.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8978003816c7b9eabe217f88c78bc26adc8f9304bf6a594b02e5a49b2ef9c11", size = 6653765, upload-time = "2025-07-24T18:52:51.094Z" },
- { url = "https://files.pythonhosted.org/packages/fd/75/a1991dd64b331d199935e096cc9daa3415ee5ccbe9f909aa48eded7bba34/grpcio-1.74.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3d7bd6e3929fd2ea7fbc3f562e4987229ead70c9ae5f01501a46701e08f1ad9", size = 6215172, upload-time = "2025-07-24T18:52:53.282Z" },
- { url = "https://files.pythonhosted.org/packages/01/a4/7cef3dbb3b073d0ce34fd507efc44ac4c9442a0ef9fba4fb3f5c551efef5/grpcio-1.74.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:136b53c91ac1d02c8c24201bfdeb56f8b3ac3278668cbb8e0ba49c88069e1bdc", size = 6329142, upload-time = "2025-07-24T18:52:54.927Z" },
- { url = "https://files.pythonhosted.org/packages/bf/d3/587920f882b46e835ad96014087054655312400e2f1f1446419e5179a383/grpcio-1.74.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fe0f540750a13fd8e5da4b3eaba91a785eea8dca5ccd2bc2ffe978caa403090e", size = 7018632, upload-time = "2025-07-24T18:52:56.523Z" },
- { url = "https://files.pythonhosted.org/packages/1f/95/c70a3b15a0bc83334b507e3d2ae20ee8fa38d419b8758a4d838f5c2a7d32/grpcio-1.74.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4e4181bfc24413d1e3a37a0b7889bea68d973d4b45dd2bc68bb766c140718f82", size = 6509641, upload-time = "2025-07-24T18:52:58.495Z" },
- { url = "https://files.pythonhosted.org/packages/4b/06/2e7042d06247d668ae69ea6998eca33f475fd4e2855f94dcb2aa5daef334/grpcio-1.74.0-cp310-cp310-win32.whl", hash = "sha256:1733969040989f7acc3d94c22f55b4a9501a30f6aaacdbccfaba0a3ffb255ab7", size = 3817478, upload-time = "2025-07-24T18:53:00.128Z" },
- { url = "https://files.pythonhosted.org/packages/93/20/e02b9dcca3ee91124060b65bbf5b8e1af80b3b76a30f694b44b964ab4d71/grpcio-1.74.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e912d3c993a29df6c627459af58975b2e5c897d93287939b9d5065f000249b5", size = 4493971, upload-time = "2025-07-24T18:53:02.068Z" },
+ { url = "https://files.pythonhosted.org/packages/51/57/89fd829fb00a6d0bee3fbcb2c8a7aa0252d908949b6ab58bfae99d39d77e/grpcio-1.75.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:1712b5890b22547dd29f3215c5788d8fc759ce6dd0b85a6ba6e2731f2d04c088", size = 5705534, upload-time = "2025-09-26T09:00:52.225Z" },
+ { url = "https://files.pythonhosted.org/packages/76/dd/2f8536e092551cf804e96bcda79ecfbc51560b214a0f5b7ebc253f0d4664/grpcio-1.75.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8d04e101bba4b55cea9954e4aa71c24153ba6182481b487ff376da28d4ba46cf", size = 11484103, upload-time = "2025-09-26T09:00:59.457Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/3d/affe2fb897804c98d56361138e73786af8f4dd876b9d9851cfe6342b53c8/grpcio-1.75.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:683cfc70be0c1383449097cba637317e4737a357cfc185d887fd984206380403", size = 6289953, upload-time = "2025-09-26T09:01:03.699Z" },
+ { url = "https://files.pythonhosted.org/packages/87/aa/0f40b7f47a0ff10d7e482bc3af22dac767c7ff27205915f08962d5ca87a2/grpcio-1.75.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:491444c081a54dcd5e6ada57314321ae526377f498d4aa09d975c3241c5b9e1c", size = 6949785, upload-time = "2025-09-26T09:01:07.504Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/45/b04407e44050781821c84f26df71b3f7bc469923f92f9f8bc27f1406dbcc/grpcio-1.75.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce08d4e112d0d38487c2b631ec8723deac9bc404e9c7b1011426af50a79999e4", size = 6465708, upload-time = "2025-09-26T09:01:11.028Z" },
+ { url = "https://files.pythonhosted.org/packages/09/3e/4ae3ec0a4d20dcaafbb6e597defcde06399ccdc5b342f607323f3b47f0a3/grpcio-1.75.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5a2acda37fc926ccc4547977ac3e56b1df48fe200de968e8c8421f6e3093df6c", size = 7100912, upload-time = "2025-09-26T09:01:14.393Z" },
+ { url = "https://files.pythonhosted.org/packages/34/3f/a9085dab5c313bb0cb853f222d095e2477b9b8490a03634cdd8d19daa5c3/grpcio-1.75.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:745c5fe6bf05df6a04bf2d11552c7d867a2690759e7ab6b05c318a772739bd75", size = 8042497, upload-time = "2025-09-26T09:01:17.759Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/87/ea54eba931ab9ed3f999ba95f5d8d01a20221b664725bab2fe93e3dee848/grpcio-1.75.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:259526a7159d39e2db40d566fe3e8f8e034d0fb2db5bf9c00e09aace655a4c2b", size = 7493284, upload-time = "2025-09-26T09:01:20.896Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/5e/287f1bf1a998f4ac46ef45d518de3b5da08b4e86c7cb5e1108cee30b0282/grpcio-1.75.1-cp310-cp310-win32.whl", hash = "sha256:f4b29b9aabe33fed5df0a85e5f13b09ff25e2c05bd5946d25270a8bd5682dac9", size = 3950809, upload-time = "2025-09-26T09:01:23.695Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/a2/3cbfc06a4ec160dc77403b29ecb5cf76ae329eb63204fea6a7c715f1dfdb/grpcio-1.75.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf2e760978dcce7ff7d465cbc7e276c3157eedc4c27aa6de7b594c7a295d3d61", size = 4644704, upload-time = "2025-09-26T09:01:25.763Z" },
]
[[package]]
@@ -884,17 +905,17 @@ wheels = [
[[package]]
name = "hf-xet"
-version = "1.1.4"
+version = "1.1.10"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8d/11/b480bb7515db97d5b2b703927a59bbdd3f87e68d47dff5591aada467b4a9/hf_xet-1.1.4.tar.gz", hash = "sha256:875158df90cb13547752532ed73cad9dfaad3b29e203143838f67178418d08a4", size = 492082, upload-time = "2025-06-16T21:20:51.375Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/31/feeddfce1748c4a233ec1aa5b7396161c07ae1aa9b7bdbc9a72c3c7dd768/hf_xet-1.1.10.tar.gz", hash = "sha256:408aef343800a2102374a883f283ff29068055c111f003ff840733d3b715bb97", size = 487910, upload-time = "2025-09-12T20:10:27.12Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c4/62/3b41a7439930996530c64955874445012fd9044c82c60b34c5891c34fec6/hf_xet-1.1.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6591ab9f61ea82d261107ed90237e2ece972f6a7577d96f5f071208bbf255d1c", size = 2643151, upload-time = "2025-06-16T21:20:45.656Z" },
- { url = "https://files.pythonhosted.org/packages/9b/9f/1744fb1d79e0ac147578b193ce29208ebb9f4636e8cdf505638f6f0a6874/hf_xet-1.1.4-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:071b0b4d4698990f746edd666c7cc42555833d22035d88db0df936677fb57d29", size = 2510687, upload-time = "2025-06-16T21:20:43.754Z" },
- { url = "https://files.pythonhosted.org/packages/d1/a8/49a81d4f81b0d21cc758b6fca3880a85ca0d209e8425c8b3a6ef694881ca/hf_xet-1.1.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b610831e92e41182d4c028653978b844d332d492cdcba1b920d3aca4a0207e", size = 3057631, upload-time = "2025-06-16T21:20:42.006Z" },
- { url = "https://files.pythonhosted.org/packages/bf/8b/65fa08273789dafbc38d0f0bdd20df56b63ebc6566981bbaa255d9d84a33/hf_xet-1.1.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f6578bcd71393abfd60395279cc160ca808b61f5f9d535b922fcdcd3f77a708d", size = 2949250, upload-time = "2025-06-16T21:20:39.914Z" },
- { url = "https://files.pythonhosted.org/packages/8b/4b/224340bb1d5c63b6e03e04095b4e42230848454bf4293c45cd7bdaa0c208/hf_xet-1.1.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fb2bbfa2aae0e4f0baca988e7ba8d8c1a39a25adf5317461eb7069ad00505b3e", size = 3124670, upload-time = "2025-06-16T21:20:47.688Z" },
- { url = "https://files.pythonhosted.org/packages/4a/b7/4be010014de6585401c32a04c46b09a4a842d66bd16ed549a401e973b74b/hf_xet-1.1.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:73346ba3e2e15ea8909a26b0862b458f15b003e6277935e3fba5bf273508d698", size = 3234131, upload-time = "2025-06-16T21:20:49.535Z" },
- { url = "https://files.pythonhosted.org/packages/c2/2d/cf148d532f741fbf93f380ff038a33c1309d1e24ea629dc39d11dca08c92/hf_xet-1.1.4-cp37-abi3-win_amd64.whl", hash = "sha256:52e8f8bc2029d8b911493f43cea131ac3fa1f0dc6a13c50b593c4516f02c6fc3", size = 2695589, upload-time = "2025-06-16T21:20:53.151Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/a2/343e6d05de96908366bdc0081f2d8607d61200be2ac802769c4284cc65bd/hf_xet-1.1.10-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:686083aca1a6669bc85c21c0563551cbcdaa5cf7876a91f3d074a030b577231d", size = 2761466, upload-time = "2025-09-12T20:10:22.836Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f9/6215f948ac8f17566ee27af6430ea72045e0418ce757260248b483f4183b/hf_xet-1.1.10-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:71081925383b66b24eedff3013f8e6bbd41215c3338be4b94ba75fd75b21513b", size = 2623807, upload-time = "2025-09-12T20:10:21.118Z" },
+ { url = "https://files.pythonhosted.org/packages/15/07/86397573efefff941e100367bbda0b21496ffcdb34db7ab51912994c32a2/hf_xet-1.1.10-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6bceb6361c80c1cc42b5a7b4e3efd90e64630bcf11224dcac50ef30a47e435", size = 3186960, upload-time = "2025-09-12T20:10:19.336Z" },
+ { url = "https://files.pythonhosted.org/packages/01/a7/0b2e242b918cc30e1f91980f3c4b026ff2eedaf1e2ad96933bca164b2869/hf_xet-1.1.10-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eae7c1fc8a664e54753ffc235e11427ca61f4b0477d757cc4eb9ae374b69f09c", size = 3087167, upload-time = "2025-09-12T20:10:17.255Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/25/3e32ab61cc7145b11eee9d745988e2f0f4fafda81b25980eebf97d8cff15/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0a0005fd08f002180f7a12d4e13b22be277725bc23ed0529f8add5c7a6309c06", size = 3248612, upload-time = "2025-09-12T20:10:24.093Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/3d/ab7109e607ed321afaa690f557a9ada6d6d164ec852fd6bf9979665dc3d6/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f", size = 3353360, upload-time = "2025-09-12T20:10:25.563Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/0e/471f0a21db36e71a2f1752767ad77e92d8cde24e974e03d662931b1305ec/hf_xet-1.1.10-cp37-abi3-win_amd64.whl", hash = "sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045", size = 2804691, upload-time = "2025-09-12T20:10:28.433Z" },
]
[[package]]
@@ -925,14 +946,14 @@ wheels = [
[[package]]
name = "httplib2"
-version = "0.22.0"
+version = "0.31.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyparsing" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3d/ad/2371116b22d616c194aa25ec410c9c6c37f23599dcd590502b74db197584/httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81", size = 351116, upload-time = "2023-03-21T22:29:37.214Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/77/6653db69c1f7ecfe5e3f9726fdadc981794656fcd7d98c4209fecfea9993/httplib2-0.31.0.tar.gz", hash = "sha256:ac7ab497c50975147d4f7b1ade44becc7df2f8954d42b38b3d69c515f531135c", size = 250759, upload-time = "2025-09-11T12:16:03.403Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854, upload-time = "2023-03-21T22:29:35.683Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/a2/0d269db0f6163be503775dc8b6a6fa15820cc9fdc866f6ba608d86b721f2/httplib2-0.31.0-py3-none-any.whl", hash = "sha256:b9cd78abea9b4e43a7714c6e0f8b6b8561a6fc1e95d5dbd367f5bf0ef35f5d24", size = 91148, upload-time = "2025-09-11T12:16:01.803Z" },
]
[[package]]
@@ -972,16 +993,16 @@ socks = [
[[package]]
name = "httpx-sse"
-version = "0.4.0"
+version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" },
+ { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" },
]
[[package]]
name = "huggingface-hub"
-version = "0.33.0"
+version = "0.35.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -993,9 +1014,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/91/8a/1362d565fefabaa4185cf3ae842a98dbc5b35146f5694f7080f043a6952f/huggingface_hub-0.33.0.tar.gz", hash = "sha256:aa31f70d29439d00ff7a33837c03f1f9dd83971ce4e29ad664d63ffb17d3bb97", size = 426179, upload-time = "2025-06-11T17:08:07.913Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/42/0e7be334a6851cd7d51cc11717cb95e89333ebf0064431c0255c56957526/huggingface_hub-0.35.1.tar.gz", hash = "sha256:3585b88c5169c64b7e4214d0e88163d4a709de6d1a502e0cd0459e9ee2c9c572", size = 461374, upload-time = "2025-09-23T13:43:47.074Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/33/fb/53587a89fbc00799e4179796f51b3ad713c5de6bb680b2becb6d37c94649/huggingface_hub-0.33.0-py3-none-any.whl", hash = "sha256:e8668875b40c68f9929150d99727d39e5ebb8a05a98e4191b908dc7ded9074b3", size = 514799, upload-time = "2025-06-11T17:08:05.757Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/60/4acf0c8a3925d9ff491dc08fe84d37e09cfca9c3b885e0db3d4dedb98cea/huggingface_hub-0.35.1-py3-none-any.whl", hash = "sha256:2f0e2709c711e3040e31d3e0418341f7092910f1462dd00350c4e97af47280a8", size = 563340, upload-time = "2025-09-23T13:43:45.343Z" },
]
[[package]]
@@ -1078,22 +1099,22 @@ wheels = [
[[package]]
name = "jiter"
-version = "0.10.0"
+version = "0.11.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9d/c0/a3bb4cc13aced219dd18191ea66e874266bd8aa7b96744e495e1c733aa2d/jiter-0.11.0.tar.gz", hash = "sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4", size = 167094, upload-time = "2025-09-15T09:20:38.212Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/be/7e/4011b5c77bec97cb2b572f566220364e3e21b51c48c5bd9c4a9c26b41b67/jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303", size = 317215, upload-time = "2025-05-18T19:03:04.303Z" },
- { url = "https://files.pythonhosted.org/packages/8a/4f/144c1b57c39692efc7ea7d8e247acf28e47d0912800b34d0ad815f6b2824/jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e", size = 322814, upload-time = "2025-05-18T19:03:06.433Z" },
- { url = "https://files.pythonhosted.org/packages/63/1f/db977336d332a9406c0b1f0b82be6f71f72526a806cbb2281baf201d38e3/jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f", size = 345237, upload-time = "2025-05-18T19:03:07.833Z" },
- { url = "https://files.pythonhosted.org/packages/d7/1c/aa30a4a775e8a672ad7f21532bdbfb269f0706b39c6ff14e1f86bdd9e5ff/jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224", size = 370999, upload-time = "2025-05-18T19:03:09.338Z" },
- { url = "https://files.pythonhosted.org/packages/35/df/f8257abc4207830cb18880781b5f5b716bad5b2a22fb4330cfd357407c5b/jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7", size = 491109, upload-time = "2025-05-18T19:03:11.13Z" },
- { url = "https://files.pythonhosted.org/packages/06/76/9e1516fd7b4278aa13a2cc7f159e56befbea9aa65c71586305e7afa8b0b3/jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6", size = 388608, upload-time = "2025-05-18T19:03:12.911Z" },
- { url = "https://files.pythonhosted.org/packages/6d/64/67750672b4354ca20ca18d3d1ccf2c62a072e8a2d452ac3cf8ced73571ef/jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf", size = 352454, upload-time = "2025-05-18T19:03:14.741Z" },
- { url = "https://files.pythonhosted.org/packages/96/4d/5c4e36d48f169a54b53a305114be3efa2bbffd33b648cd1478a688f639c1/jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90", size = 391833, upload-time = "2025-05-18T19:03:16.426Z" },
- { url = "https://files.pythonhosted.org/packages/0b/de/ce4a6166a78810bd83763d2fa13f85f73cbd3743a325469a4a9289af6dae/jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0", size = 523646, upload-time = "2025-05-18T19:03:17.704Z" },
- { url = "https://files.pythonhosted.org/packages/a2/a6/3bc9acce53466972964cf4ad85efecb94f9244539ab6da1107f7aed82934/jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee", size = 514735, upload-time = "2025-05-18T19:03:19.44Z" },
- { url = "https://files.pythonhosted.org/packages/b4/d8/243c2ab8426a2a4dea85ba2a2ba43df379ccece2145320dfd4799b9633c5/jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4", size = 210747, upload-time = "2025-05-18T19:03:21.184Z" },
- { url = "https://files.pythonhosted.org/packages/37/7a/8021bd615ef7788b98fc76ff533eaac846322c170e93cbffa01979197a45/jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5", size = 207484, upload-time = "2025-05-18T19:03:23.046Z" },
+ { url = "https://files.pythonhosted.org/packages/25/21/7dd1235a19e26979be6098e87e4cced2e061752f3a40a17bbce6dea7fae1/jiter-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3893ce831e1c0094a83eeaf56c635a167d6fa8cc14393cc14298fd6fdc2a2449", size = 309875, upload-time = "2025-09-15T09:18:48.41Z" },
+ { url = "https://files.pythonhosted.org/packages/71/f9/462b54708aa85b135733ccba70529dd68a18511bf367a87c5fd28676c841/jiter-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:25c625b9b61b5a8725267fdf867ef2e51b429687f6a4eef211f4612e95607179", size = 316505, upload-time = "2025-09-15T09:18:51.057Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/40/14e2eeaac6a47bff27d213834795472355fd39769272eb53cb7aa83d5aa8/jiter-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4ca85fb6a62cf72e1c7f5e34ddef1b660ce4ed0886ec94a1ef9777d35eaa1f", size = 337613, upload-time = "2025-09-15T09:18:52.358Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ed/a5f1f8419c92b150a7c7fb5ccba1fb1e192887ad713d780e70874f0ce996/jiter-0.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:572208127034725e79c28437b82414028c3562335f2b4f451d98136d0fc5f9cd", size = 361438, upload-time = "2025-09-15T09:18:54.637Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/f5/70682c023dfcdd463a53faf5d30205a7d99c51d70d3e303c932d0936e5a2/jiter-0.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494ba627c7f550ad3dabb21862864b8f2216098dc18ff62f37b37796f2f7c325", size = 486180, upload-time = "2025-09-15T09:18:56.158Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/39/020d08cbab4eab48142ad88b837c41eb08a15c0767fdb7c0d3265128a44b/jiter-0.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8da18a99f58bca3ecc2d2bba99cac000a924e115b6c4f0a2b98f752b6fbf39a", size = 376681, upload-time = "2025-09-15T09:18:57.553Z" },
+ { url = "https://files.pythonhosted.org/packages/52/10/b86733f6e594cf51dd142f37c602d8df87c554c5844958deaab0de30eb5d/jiter-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ffd3b0fff3fabbb02cc09910c08144db6bb5697a98d227a074401e01ee63dd", size = 348685, upload-time = "2025-09-15T09:18:59.208Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/ee/8861665e83a9e703aa5f65fddddb6225428e163e6b0baa95a7f9a8fb9aae/jiter-0.11.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fe6530aa738a4f7d4e4702aa8f9581425d04036a5f9e25af65ebe1f708f23be", size = 385573, upload-time = "2025-09-15T09:19:00.593Z" },
+ { url = "https://files.pythonhosted.org/packages/25/74/05afec03600951f128293813b5a208c9ba1bf587c57a344c05a42a69e1b1/jiter-0.11.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e35d66681c133a03d7e974e7eedae89720fe8ca3bd09f01a4909b86a8adf31f5", size = 516669, upload-time = "2025-09-15T09:19:02.369Z" },
+ { url = "https://files.pythonhosted.org/packages/93/d1/2e5bfe147cfbc2a5eef7f73eb75dc5c6669da4fa10fc7937181d93af9495/jiter-0.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c59459beca2fbc9718b6f1acb7bfb59ebc3eb4294fa4d40e9cb679dafdcc6c60", size = 508767, upload-time = "2025-09-15T09:19:04.011Z" },
+ { url = "https://files.pythonhosted.org/packages/87/50/597f71307e10426b5c082fd05d38c615ddbdd08c3348d8502963307f0652/jiter-0.11.0-cp310-cp310-win32.whl", hash = "sha256:b7b0178417b0dcfc5f259edbc6db2b1f5896093ed9035ee7bab0f2be8854726d", size = 205476, upload-time = "2025-09-15T09:19:05.594Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/86/1e5214b3272e311754da26e63edec93a183811d4fc2e0118addec365df8b/jiter-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:11df2bf99fb4754abddd7f5d940a48e51f9d11624d6313ca4314145fcad347f0", size = 204708, upload-time = "2025-09-15T09:19:06.955Z" },
]
[[package]]
@@ -1107,7 +1128,7 @@ wheels = [
[[package]]
name = "jsonschema"
-version = "4.24.0"
+version = "4.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
@@ -1115,21 +1136,21 @@ dependencies = [
{ name = "referencing" },
{ name = "rpds-py" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
]
[[package]]
name = "jsonschema-specifications"
-version = "2025.4.1"
+version = "2025.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "referencing" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" },
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
]
[[package]]
@@ -1147,33 +1168,32 @@ wheels = [
[[package]]
name = "lxml"
-version = "5.4.0"
+version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f5/1f/a3b6b74a451ceb84b471caa75c934d2430a4d84395d38ef201d539f38cd1/lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c", size = 8076838, upload-time = "2025-04-23T01:44:29.325Z" },
- { url = "https://files.pythonhosted.org/packages/36/af/a567a55b3e47135b4d1f05a1118c24529104c003f95851374b3748139dc1/lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7", size = 4381827, upload-time = "2025-04-23T01:44:33.345Z" },
- { url = "https://files.pythonhosted.org/packages/50/ba/4ee47d24c675932b3eb5b6de77d0f623c2db6dc466e7a1f199792c5e3e3a/lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf", size = 5204098, upload-time = "2025-04-23T01:44:35.809Z" },
- { url = "https://files.pythonhosted.org/packages/f2/0f/b4db6dfebfefe3abafe360f42a3d471881687fd449a0b86b70f1f2683438/lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28", size = 4930261, upload-time = "2025-04-23T01:44:38.271Z" },
- { url = "https://files.pythonhosted.org/packages/0b/1f/0bb1bae1ce056910f8db81c6aba80fec0e46c98d77c0f59298c70cd362a3/lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609", size = 5529621, upload-time = "2025-04-23T01:44:40.921Z" },
- { url = "https://files.pythonhosted.org/packages/21/f5/e7b66a533fc4a1e7fa63dd22a1ab2ec4d10319b909211181e1ab3e539295/lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4", size = 4983231, upload-time = "2025-04-23T01:44:43.871Z" },
- { url = "https://files.pythonhosted.org/packages/11/39/a38244b669c2d95a6a101a84d3c85ba921fea827e9e5483e93168bf1ccb2/lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7", size = 5084279, upload-time = "2025-04-23T01:44:46.632Z" },
- { url = "https://files.pythonhosted.org/packages/db/64/48cac242347a09a07740d6cee7b7fd4663d5c1abd65f2e3c60420e231b27/lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f", size = 4927405, upload-time = "2025-04-23T01:44:49.843Z" },
- { url = "https://files.pythonhosted.org/packages/98/89/97442835fbb01d80b72374f9594fe44f01817d203fa056e9906128a5d896/lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997", size = 5550169, upload-time = "2025-04-23T01:44:52.791Z" },
- { url = "https://files.pythonhosted.org/packages/f1/97/164ca398ee654eb21f29c6b582685c6c6b9d62d5213abc9b8380278e9c0a/lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c", size = 5062691, upload-time = "2025-04-23T01:44:56.108Z" },
- { url = "https://files.pythonhosted.org/packages/d0/bc/712b96823d7feb53482d2e4f59c090fb18ec7b0d0b476f353b3085893cda/lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b", size = 5133503, upload-time = "2025-04-23T01:44:59.222Z" },
- { url = "https://files.pythonhosted.org/packages/d4/55/a62a39e8f9da2a8b6002603475e3c57c870cd9c95fd4b94d4d9ac9036055/lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b", size = 4999346, upload-time = "2025-04-23T01:45:02.088Z" },
- { url = "https://files.pythonhosted.org/packages/ea/47/a393728ae001b92bb1a9e095e570bf71ec7f7fbae7688a4792222e56e5b9/lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563", size = 5627139, upload-time = "2025-04-23T01:45:04.582Z" },
- { url = "https://files.pythonhosted.org/packages/5e/5f/9dcaaad037c3e642a7ea64b479aa082968de46dd67a8293c541742b6c9db/lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5", size = 5465609, upload-time = "2025-04-23T01:45:07.649Z" },
- { url = "https://files.pythonhosted.org/packages/a7/0a/ebcae89edf27e61c45023005171d0ba95cb414ee41c045ae4caf1b8487fd/lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776", size = 5192285, upload-time = "2025-04-23T01:45:10.456Z" },
- { url = "https://files.pythonhosted.org/packages/42/ad/cc8140ca99add7d85c92db8b2354638ed6d5cc0e917b21d36039cb15a238/lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7", size = 3477507, upload-time = "2025-04-23T01:45:12.474Z" },
- { url = "https://files.pythonhosted.org/packages/e9/39/597ce090da1097d2aabd2f9ef42187a6c9c8546d67c419ce61b88b336c85/lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250", size = 3805104, upload-time = "2025-04-23T01:45:15.104Z" },
- { url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319, upload-time = "2025-04-23T01:49:22.069Z" },
- { url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614, upload-time = "2025-04-23T01:49:24.599Z" },
- { url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273, upload-time = "2025-04-23T01:49:27.355Z" },
- { url = "https://files.pythonhosted.org/packages/be/7c/8c3f15df2ca534589717bfd19d1e3482167801caedfa4d90a575facf68a6/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37", size = 4208552, upload-time = "2025-04-23T01:49:29.949Z" },
- { url = "https://files.pythonhosted.org/packages/7d/d8/9567afb1665f64d73fc54eb904e418d1138d7f011ed00647121b4dd60b38/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571", size = 4331091, upload-time = "2025-04-23T01:49:32.842Z" },
- { url = "https://files.pythonhosted.org/packages/f1/ab/fdbbd91d8d82bf1a723ba88ec3e3d76c022b53c391b0c13cad441cdb8f9e/lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4", size = 3487862, upload-time = "2025-04-23T01:49:36.296Z" },
+ { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" },
+ { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" },
+ { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" },
+ { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" },
+ { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" },
+ { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" },
+ { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" },
+ { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" },
+ { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" },
]
[[package]]
@@ -1196,49 +1216,52 @@ wheels = [
[[package]]
name = "mammoth"
-version = "1.9.1"
+version = "1.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cobble" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/23/f8/b48bf3b9c7c47f3bc0de7630f0f180c01e92570953611089489d34542253/mammoth-1.9.1.tar.gz", hash = "sha256:7924254ab8f03efe55fadc0fd5f7828db831190eb2679d63cb4372873e71c572", size = 51056, upload-time = "2025-05-28T19:17:56.332Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/89/0d/2ab86f37021b4c50fe72354acd226b1e31a10497e51f6cbd7e3d1eca1181/mammoth-1.10.0.tar.gz", hash = "sha256:cb6fbba41ccf8b5502859c457177d87a833fef0e0b1d4e6fd23ec372fe892c30", size = 52285, upload-time = "2025-08-02T15:40:55.849Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/be/0c/3153f159b78e368ac473a00e955d69d976e4b69740ed07c76c9f72a161b8/mammoth-1.9.1-py2.py3-none-any.whl", hash = "sha256:f0569bd640cee6c77a07e7c75c5dc10d745dc4dc95d530cfcbb0a5d9536d636c", size = 52991, upload-time = "2025-05-28T19:17:54.62Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/67/36eeb3a8726df3b282ba99ec126323871cffdbcf3b7a1db64ca9bbe4abc1/mammoth-1.10.0-py2.py3-none-any.whl", hash = "sha256:a1c87d5b98ca30230394267f98614b58b14b50f8031dc33ac9a535c6ab04eb99", size = 53823, upload-time = "2025-08-02T15:40:54.255Z" },
]
[[package]]
name = "markdownify"
-version = "1.1.0"
+version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
{ name = "six" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2f/78/c48fed23c7aebc2c16049062e72de1da3220c274de59d28c942acdc9ffb2/markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd", size = 17127, upload-time = "2025-03-05T11:54:40.574Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/83/1b/6f2697b51eaca81f08852fd2734745af15718fea10222a1d40f8a239c4ea/markdownify-1.2.0.tar.gz", hash = "sha256:f6c367c54eb24ee953921804dfe6d6575c5e5b42c643955e7242034435de634c", size = 18771, upload-time = "2025-08-09T17:44:15.302Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/64/11/b751af7ad41b254a802cf52f7bc1fca7cabe2388132f2ce60a1a6b9b9622/markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef", size = 13901, upload-time = "2025-03-05T11:54:39.454Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/e2/7af643acb4cae0741dffffaa7f3f7c9e7ab4046724543ba1777c401d821c/markdownify-1.2.0-py3-none-any.whl", hash = "sha256:48e150a1c4993d4d50f282f725c0111bd9eb25645d41fa2f543708fd44161351", size = 15561, upload-time = "2025-08-09T17:44:14.074Z" },
]
[[package]]
name = "markitdown"
-version = "0.1.1"
+version = "0.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
{ name = "charset-normalizer" },
+ { name = "defusedxml" },
{ name = "magika" },
{ name = "markdownify" },
+ { name = "onnxruntime", marker = "sys_platform == 'win32'" },
{ name = "requests" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/cb/e8/83669ba97718bbbccd4c432b763d22783df4c8218e770717151acf01e85b/markitdown-0.1.1.tar.gz", hash = "sha256:da97a55a45a3d775ea758e88a344d5cac94ee97115fb0293f99027d32c2fc3f6", size = 31475, upload-time = "2025-03-25T06:22:21.438Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/87/31/90cef2bc8ecd85c200ed3b3d1e20fc7a724213502685c4b05b5431e02668/markitdown-0.1.3.tar.gz", hash = "sha256:b0d9127c3373a68274dede6af6c9bb0684b78ce364c727c4c304da97a20d6fd9", size = 40039, upload-time = "2025-08-26T22:37:04.4Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/8a/c1f85ee609de5d45f80d0213bebf6664f76ab406e9d57709e684a4a436ba/markitdown-0.1.1-py3-none-any.whl", hash = "sha256:98ea8c009fe174b37ef933e00f4364214e8fed35691178b8521b13604d0c4a58", size = 48230, upload-time = "2025-03-25T06:22:19.773Z" },
+ { url = "https://files.pythonhosted.org/packages/97/83/7b47d2ecbf58650a03aeeb21ba2d59175f202bf4fb81d44f40f1deb82bc0/markitdown-0.1.3-py3-none-any.whl", hash = "sha256:08d9a25770979d78f60dcc0afcb868de6799608e4db65342b2e03304fb091251", size = 58391, upload-time = "2025-08-26T22:37:02.924Z" },
]
[package.optional-dependencies]
all = [
{ name = "azure-ai-documentintelligence" },
{ name = "azure-identity" },
+ { name = "lxml" },
{ name = "mammoth" },
{ name = "olefile" },
{ name = "openpyxl" },
@@ -1253,22 +1276,24 @@ all = [
[[package]]
name = "mcp"
-version = "1.9.4"
+version = "1.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "httpx-sse" },
+ { name = "jsonschema" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "sse-starlette" },
{ name = "starlette" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/06/f2/dc2450e566eeccf92d89a00c3e813234ad58e2ba1e31d11467a09ac4f3b9/mcp-1.9.4.tar.gz", hash = "sha256:cfb0bcd1a9535b42edaef89947b9e18a8feb49362e1cc059d6e7fc636f2cb09f", size = 333294, upload-time = "2025-06-12T08:20:30.158Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/9e/e65114795f359f314d7061f4fcb50dfe60026b01b52ad0b986b4631bf8bb/mcp-1.15.0.tar.gz", hash = "sha256:5bda1f4d383cf539d3c035b3505a3de94b20dbd7e4e8b4bd071e14634eeb2d72", size = 469622, upload-time = "2025-09-25T15:39:51.995Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232, upload-time = "2025-06-12T08:20:28.551Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/82/4d0df23d5ff5bb982a59ad597bc7cb9920f2650278ccefb8e0d85c5ce3d4/mcp-1.15.0-py3-none-any.whl", hash = "sha256:314614c8addc67b663d6c3e4054db0a5c3dedc416c24ef8ce954e203fdc2333d", size = 166963, upload-time = "2025-09-25T15:39:50.538Z" },
]
[[package]]
@@ -1305,11 +1330,11 @@ wheels = [
[[package]]
name = "more-itertools"
-version = "10.7.0"
+version = "10.8.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
]
[[package]]
@@ -1323,16 +1348,16 @@ wheels = [
[[package]]
name = "msal"
-version = "1.33.0"
+version = "1.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyjwt", extra = ["crypto"] },
{ name = "requests" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d5/da/81acbe0c1fd7e9e4ec35f55dadeba9833a847b9a6ba2e2d1e4432da901dd/msal-1.33.0.tar.gz", hash = "sha256:836ad80faa3e25a7d71015c990ce61f704a87328b1e73bcbb0623a18cbf17510", size = 153801, upload-time = "2025-07-22T19:36:33.693Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961, upload-time = "2025-09-22T23:05:48.989Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/86/5b/fbc73e91f7727ae1e79b21ed833308e99dc11cc1cd3d4717f579775de5e9/msal-1.33.0-py3-none-any.whl", hash = "sha256:c0cd41cecf8eaed733ee7e3be9e040291eba53b0f262d3ae9c58f38b04244273", size = 116853, upload-time = "2025-07-22T19:36:32.403Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/dc/18d48843499e278538890dc709e9ee3dea8375f8be8e82682851df1b48b5/msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1", size = 116987, upload-time = "2025-09-22T23:05:47.294Z" },
]
[[package]]
@@ -1349,32 +1374,32 @@ wheels = [
[[package]]
name = "multidict"
-version = "6.5.0"
+version = "6.6.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/46/b5/59f27b4ce9951a4bce56b88ba5ff5159486797ab18863f2b4c1c5e8465bd/multidict-6.5.0.tar.gz", hash = "sha256:942bd8002492ba819426a8d7aefde3189c1b87099cdf18aaaefefcf7f3f7b6d2", size = 98512, upload-time = "2025-06-17T14:15:56.556Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/88/f8354ef1cb1121234c3461ff3d11eac5f4fe115f00552d3376306275c9ab/multidict-6.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e118a202904623b1d2606d1c8614e14c9444b59d64454b0c355044058066469", size = 73858, upload-time = "2025-06-17T14:13:21.451Z" },
- { url = "https://files.pythonhosted.org/packages/49/04/634b49c7abe71bd1c61affaeaa0c2a46b6be8d599a07b495259615dbdfe0/multidict-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a42995bdcaff4e22cb1280ae7752c3ed3fbb398090c6991a2797a4a0e5ed16a9", size = 43186, upload-time = "2025-06-17T14:13:23.615Z" },
- { url = "https://files.pythonhosted.org/packages/3b/ff/091ff4830ec8f96378578bfffa7f324a9dd16f60274cec861ae65ba10be3/multidict-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2261b538145723ca776e55208640fffd7ee78184d223f37c2b40b9edfe0e818a", size = 43031, upload-time = "2025-06-17T14:13:24.725Z" },
- { url = "https://files.pythonhosted.org/packages/10/c1/1b4137845f8b8dbc2332af54e2d7761c6a29c2c33c8d47a0c8c70676bac1/multidict-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e5b19f8cd67235fab3e195ca389490415d9fef5a315b1fa6f332925dc924262", size = 233588, upload-time = "2025-06-17T14:13:26.181Z" },
- { url = "https://files.pythonhosted.org/packages/c3/77/cbe9a1f58c6d4f822663788e414637f256a872bc352cedbaf7717b62db58/multidict-6.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:177b081e4dec67c3320b16b3aa0babc178bbf758553085669382c7ec711e1ec8", size = 222714, upload-time = "2025-06-17T14:13:27.482Z" },
- { url = "https://files.pythonhosted.org/packages/6c/37/39e1142c2916973818515adc13bbdb68d3d8126935e3855200e059a79bab/multidict-6.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d30a2cc106a7d116b52ee046207614db42380b62e6b1dd2a50eba47c5ca5eb1", size = 242741, upload-time = "2025-06-17T14:13:28.92Z" },
- { url = "https://files.pythonhosted.org/packages/a3/aa/60c3ef0c87ccad3445bf01926a1b8235ee24c3dde483faef1079cc91706d/multidict-6.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a72933bc308d7a64de37f0d51795dbeaceebdfb75454f89035cdfc6a74cfd129", size = 235008, upload-time = "2025-06-17T14:13:30.587Z" },
- { url = "https://files.pythonhosted.org/packages/bf/5e/f7e0fd5f5b8a7b9a75b0f5642ca6b6dde90116266920d8cf63b513f3908b/multidict-6.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d109e663d032280ef8ef62b50924b2e887d5ddf19e301844a6cb7e91a172a6", size = 226627, upload-time = "2025-06-17T14:13:31.831Z" },
- { url = "https://files.pythonhosted.org/packages/b7/74/1bc0a3c6a9105051f68a6991fe235d7358836e81058728c24d5bbdd017cb/multidict-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b555329c9894332401f03b9a87016f0b707b6fccd4706793ec43b4a639e75869", size = 228232, upload-time = "2025-06-17T14:13:33.402Z" },
- { url = "https://files.pythonhosted.org/packages/99/e7/37118291cdc31f4cc680d54047cdea9b520e9a724a643919f71f8c2a2aeb/multidict-6.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6994bad9d471ef2156f2b6850b51e20ee409c6b9deebc0e57be096be9faffdce", size = 246616, upload-time = "2025-06-17T14:13:34.964Z" },
- { url = "https://files.pythonhosted.org/packages/ff/89/e2c08d6bdb21a1a55be4285510d058ace5f5acabe6b57900432e863d4c70/multidict-6.5.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:b15f817276c96cde9060569023808eec966bd8da56a97e6aa8116f34ddab6534", size = 235007, upload-time = "2025-06-17T14:13:36.428Z" },
- { url = "https://files.pythonhosted.org/packages/89/1e/e39a98e8e1477ec7a871b3c17265658fbe6d617048059ae7fa5011b224f3/multidict-6.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b4bf507c991db535a935b2127cf057a58dbc688c9f309c72080795c63e796f58", size = 244824, upload-time = "2025-06-17T14:13:37.982Z" },
- { url = "https://files.pythonhosted.org/packages/a3/ba/63e11edd45c31e708c5a1904aa7ac4de01e13135a04cfe96bc71eb359b85/multidict-6.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:60c3f8f13d443426c55f88cf3172547bbc600a86d57fd565458b9259239a6737", size = 257229, upload-time = "2025-06-17T14:13:39.554Z" },
- { url = "https://files.pythonhosted.org/packages/0f/00/bdcceb6af424936adfc8b92a79d3a95863585f380071393934f10a63f9e3/multidict-6.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a10227168a24420c158747fc201d4279aa9af1671f287371597e2b4f2ff21879", size = 247118, upload-time = "2025-06-17T14:13:40.795Z" },
- { url = "https://files.pythonhosted.org/packages/b6/a0/4aa79e991909cca36ca821a9ba5e8e81e4cd5b887c81f89ded994e0f49df/multidict-6.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e3b1425fe54ccfde66b8cfb25d02be34d5dfd2261a71561ffd887ef4088b4b69", size = 243948, upload-time = "2025-06-17T14:13:42.477Z" },
- { url = "https://files.pythonhosted.org/packages/21/8b/e45e19ce43afb31ff6b0fd5d5816b4fcc1fcc2f37e8a82aefae06c40c7a6/multidict-6.5.0-cp310-cp310-win32.whl", hash = "sha256:b4e47ef51237841d1087e1e1548071a6ef22e27ed0400c272174fa585277c4b4", size = 40433, upload-time = "2025-06-17T14:13:43.972Z" },
- { url = "https://files.pythonhosted.org/packages/d2/6e/96e0ba4601343d9344e69503fca072ace19c35f7d4ca3d68401e59acdc8f/multidict-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:63b3b24fadc7067282c88fae5b2f366d5b3a7c15c021c2838de8c65a50eeefb4", size = 44423, upload-time = "2025-06-17T14:13:44.991Z" },
- { url = "https://files.pythonhosted.org/packages/eb/4a/9befa919d7a390f13a5511a69282b7437782071160c566de6e0ebf712c9f/multidict-6.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:8b2d61afbafc679b7eaf08e9de4fa5d38bd5dc7a9c0a577c9f9588fb49f02dbb", size = 41481, upload-time = "2025-06-17T14:13:49.389Z" },
- { url = "https://files.pythonhosted.org/packages/44/d8/45e8fc9892a7386d074941429e033adb4640e59ff0780d96a8cf46fe788e/multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc", size = 12181, upload-time = "2025-06-17T14:15:55.156Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6b/86f353088c1358e76fd30b0146947fddecee812703b604ee901e85cd2a80/multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", size = 77054, upload-time = "2025-08-11T12:06:02.99Z" },
+ { url = "https://files.pythonhosted.org/packages/19/5d/c01dc3d3788bb877bd7f5753ea6eb23c1beeca8044902a8f5bfb54430f63/multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", size = 44914, upload-time = "2025-08-11T12:06:05.264Z" },
+ { url = "https://files.pythonhosted.org/packages/46/44/964dae19ea42f7d3e166474d8205f14bb811020e28bc423d46123ddda763/multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", size = 44601, upload-time = "2025-08-11T12:06:06.627Z" },
+ { url = "https://files.pythonhosted.org/packages/31/20/0616348a1dfb36cb2ab33fc9521de1f27235a397bf3f59338e583afadd17/multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", size = 224821, upload-time = "2025-08-11T12:06:08.06Z" },
+ { url = "https://files.pythonhosted.org/packages/14/26/5d8923c69c110ff51861af05bd27ca6783011b96725d59ccae6d9daeb627/multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", size = 242608, upload-time = "2025-08-11T12:06:09.697Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/cc/e2ad3ba9459aa34fa65cf1f82a5c4a820a2ce615aacfb5143b8817f76504/multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", size = 222324, upload-time = "2025-08-11T12:06:10.905Z" },
+ { url = "https://files.pythonhosted.org/packages/19/db/4ed0f65701afbc2cb0c140d2d02928bb0fe38dd044af76e58ad7c54fd21f/multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", size = 253234, upload-time = "2025-08-11T12:06:12.658Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c1/5160c9813269e39ae14b73debb907bfaaa1beee1762da8c4fb95df4764ed/multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", size = 251613, upload-time = "2025-08-11T12:06:13.97Z" },
+ { url = "https://files.pythonhosted.org/packages/05/a9/48d1bd111fc2f8fb98b2ed7f9a115c55a9355358432a19f53c0b74d8425d/multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", size = 241649, upload-time = "2025-08-11T12:06:15.204Z" },
+ { url = "https://files.pythonhosted.org/packages/85/2a/f7d743df0019408768af8a70d2037546a2be7b81fbb65f040d76caafd4c5/multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", size = 239238, upload-time = "2025-08-11T12:06:16.467Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/b8/4f4bb13323c2d647323f7919201493cf48ebe7ded971717bfb0f1a79b6bf/multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", size = 233517, upload-time = "2025-08-11T12:06:18.107Z" },
+ { url = "https://files.pythonhosted.org/packages/33/29/4293c26029ebfbba4f574febd2ed01b6f619cfa0d2e344217d53eef34192/multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", size = 243122, upload-time = "2025-08-11T12:06:19.361Z" },
+ { url = "https://files.pythonhosted.org/packages/20/60/a1c53628168aa22447bfde3a8730096ac28086704a0d8c590f3b63388d0c/multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", size = 248992, upload-time = "2025-08-11T12:06:20.661Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/3b/55443a0c372f33cae5d9ec37a6a973802884fa0ab3586659b197cf8cc5e9/multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", size = 243708, upload-time = "2025-08-11T12:06:21.891Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/60/a18c6900086769312560b2626b18e8cca22d9e85b1186ba77f4755b11266/multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", size = 237498, upload-time = "2025-08-11T12:06:23.206Z" },
+ { url = "https://files.pythonhosted.org/packages/11/3d/8bdd8bcaff2951ce2affccca107a404925a2beafedd5aef0b5e4a71120a6/multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", size = 41415, upload-time = "2025-08-11T12:06:24.77Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/53/cab1ad80356a4cd1b685a254b680167059b433b573e53872fab245e9fc95/multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", size = 46046, upload-time = "2025-08-11T12:06:25.893Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/9a/874212b6f5c1c2d870d0a7adc5bb4cfe9b0624fa15cdf5cf757c0f5087ae/multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", size = 43147, upload-time = "2025-08-11T12:06:27.534Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" },
]
[[package]]
@@ -1395,27 +1420,30 @@ wheels = [
[[package]]
name = "nodejs-wheel"
-version = "22.18.0"
+version = "22.19.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodejs-wheel-binaries" },
]
+sdist = { url = "https://files.pythonhosted.org/packages/3b/28/76f283d083a469b1ad26c3c711a0c9fa8405d8f6a5edfa5e17a4958aeb82/nodejs_wheel-22.19.0.tar.gz", hash = "sha256:bdd854f9b87faf8c5305dc60d99a12d56a37e0ea8c9064a84845fdeaf987dfd8", size = 2968, upload-time = "2025-09-12T10:33:45.808Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6b/a7/868d540f3c91da3ce9cbe96f348e155c0e87e50a173b09a1c6530ec2c3f0/nodejs_wheel-22.18.0-py3-none-any.whl", hash = "sha256:f49d776cfd6a2655694a8cd62cae77482a3bd852e4dce98ad73660ef78722c4c", size = 3996, upload-time = "2025-08-01T11:10:27.346Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/9a/c2ea78e11b6d28a80a7e638ac575b662860dfcf3ed83067e80c998cfaf58/nodejs_wheel-22.19.0-py3-none-any.whl", hash = "sha256:9021f544885c5b8122572bea8b23affb8a88033eaa9298e1c5e9789b5a31a84e", size = 3990, upload-time = "2025-09-12T10:33:16.166Z" },
]
[[package]]
name = "nodejs-wheel-binaries"
-version = "22.18.0"
+version = "22.19.0"
source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/ca/6033f80b7aebc23cb31ed8b09608b6308c5273c3522aedd043e8a0644d83/nodejs_wheel_binaries-22.19.0.tar.gz", hash = "sha256:e69b97ef443d36a72602f7ed356c6a36323873230f894799f4270a853932fdb3", size = 8060, upload-time = "2025-09-12T10:33:46.935Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/6d/773e09de4a052cc75c129c3766a3cf77c36bff8504a38693b735f4a1eb55/nodejs_wheel_binaries-22.18.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b04495857755c5d5658f7ac969d84f25898fe0b0c1bdc41172e5e0ac6105ca", size = 50873051, upload-time = "2025-08-01T11:10:29.475Z" },
- { url = "https://files.pythonhosted.org/packages/ae/fc/3d6fd4ad5d26c9acd46052190d6a8895dc5050297b03d9cce03def53df0d/nodejs_wheel_binaries-22.18.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:bd4d016257d4dfe604ed526c19bd4695fdc4f4cc32e8afc4738111447aa96d03", size = 51814481, upload-time = "2025-08-01T11:10:33.086Z" },
- { url = "https://files.pythonhosted.org/packages/10/f9/7be44809a861605f844077f9e731a117b669d5ca6846a7820e7dd82c9fad/nodejs_wheel_binaries-22.18.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b125f94f3f5e8ab9560d3bd637497f02e45470aeea74cf6fe60afe751cfa5f", size = 57804907, upload-time = "2025-08-01T11:10:36.83Z" },
- { url = "https://files.pythonhosted.org/packages/e9/67/563e74a0dff653ec7ddee63dc49b3f37a20df39f23675cfc801d7e8e4bb7/nodejs_wheel_binaries-22.18.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bbb81b6e67c15f04e2a9c6c220d7615fb46ae8f1ad388df0d66abac6bed5f8", size = 58335587, upload-time = "2025-08-01T11:10:40.716Z" },
- { url = "https://files.pythonhosted.org/packages/b6/b1/ec45fefef60223dd40e7953e2ff087964e200d6ec2d04eae0171d6428679/nodejs_wheel_binaries-22.18.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5d3ea8b7f957ae16b73241451f6ce831d6478156f363cce75c7ea71cbe6c6f7", size = 59662356, upload-time = "2025-08-01T11:10:44.795Z" },
- { url = "https://files.pythonhosted.org/packages/a2/ed/6de2c73499eebf49d0d20e0704f64566029a3441c48cd4f655d49befd28b/nodejs_wheel_binaries-22.18.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bcda35b07677039670102a6f9b78c2313fd526111d407cb7ffc2a4c243a48ef9", size = 60706806, upload-time = "2025-08-01T11:10:48.985Z" },
- { url = "https://files.pythonhosted.org/packages/2b/f5/487434b1792c4f28c63876e4a896f2b6e953e2dc1f0b3940e912bd087755/nodejs_wheel_binaries-22.18.0-py2.py3-none-win_amd64.whl", hash = "sha256:0f55e72733f1df2f542dce07f35145ac2e125408b5e2051cac08e5320e41b4d1", size = 39998139, upload-time = "2025-08-01T11:10:52.676Z" },
+ { url = "https://files.pythonhosted.org/packages/93/a2/0d055fd1d8c9a7a971c4db10cf42f3bba57c964beb6cf383ca053f2cdd20/nodejs_wheel_binaries-22.19.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:43eca1526455a1fb4cb777095198f7ebe5111a4444749c87f5c2b84645aaa72a", size = 50902454, upload-time = "2025-09-12T10:33:18.3Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/f5/446f7b3c5be1d2f5145ffa3c9aac3496e06cdf0f436adeb21a1f95dd79a7/nodejs_wheel_binaries-22.19.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:feb06709e1320790d34babdf71d841ec7f28e4c73217d733e7f5023060a86bfc", size = 51837860, upload-time = "2025-09-12T10:33:21.599Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/4e/d0a036f04fd0f5dc3ae505430657044b8d9853c33be6b2d122bb171aaca3/nodejs_wheel_binaries-22.19.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9f5777292491430457c99228d3a267decf12a09d31246f0692391e3513285e", size = 57841528, upload-time = "2025-09-12T10:33:25.433Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/11/4811d27819f229cc129925c170db20c12d4f01ad366a0066f06d6eb833cf/nodejs_wheel_binaries-22.19.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1392896f1a05a88a8a89b26e182d90fdf3020b4598a047807b91b65731e24c00", size = 58368815, upload-time = "2025-09-12T10:33:29.083Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/94/df41416856b980e38a7ff280cfb59f142a77955ccdbec7cc4260d8ab2e78/nodejs_wheel_binaries-22.19.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9164c876644f949cad665e3ada00f75023e18f381e78a1d7b60ccbbfb4086e73", size = 59690937, upload-time = "2025-09-12T10:33:32.771Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/39/8d0d5f84b7616bdc4eca725f5d64a1cfcac3d90cf3f30cae17d12f8e987f/nodejs_wheel_binaries-22.19.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6b4b75166134010bc9cfebd30dc57047796a27049fef3fc22316216d76bc0af7", size = 60751996, upload-time = "2025-09-12T10:33:36.962Z" },
+ { url = "https://files.pythonhosted.org/packages/41/93/2d66b5b60055dd1de6e37e35bef563c15e4cafa5cfe3a6990e0ab358e515/nodejs_wheel_binaries-22.19.0-py2.py3-none-win_amd64.whl", hash = "sha256:3f271f5abfc71b052a6b074225eca8c1223a0f7216863439b86feaca814f6e5a", size = 40026140, upload-time = "2025-09-12T10:33:40.33Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/46/c9cf7ff7e3c71f07ca8331c939afd09b6e59fc85a2944ea9411e8b29ce50/nodejs_wheel_binaries-22.19.0-py2.py3-none-win_arm64.whl", hash = "sha256:666a355fe0c9bde44a9221cd543599b029045643c8196b8eedb44f28dc192e06", size = 38804500, upload-time = "2025-09-12T10:33:43.302Z" },
]
[[package]]
@@ -1474,7 +1502,7 @@ wheels = [
[[package]]
name = "openai"
-version = "1.99.3"
+version = "1.109.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -1486,9 +1514,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/72/d3/c372420c8ca1c60e785fd8c19e536cea8f16b0cfdcdad6458e1d8884f2ea/openai-1.99.3.tar.gz", hash = "sha256:1a0e2910e4545d828c14218f2ac3276827c94a043f5353e43b9413b38b497897", size = 504932, upload-time = "2025-08-07T20:35:15.893Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133, upload-time = "2025-09-24T13:00:53.075Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/92/bc/e52f49940b4e320629da7db09c90a2407a48c612cff397b4b41b7e58cdf9/openai-1.99.3-py3-none-any.whl", hash = "sha256:c786a03f6cddadb5ee42c6d749aa4f6134fe14fdd7d69a667e5e7ce7fd29a719", size = 785776, upload-time = "2025-08-07T20:35:13.653Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" },
]
[[package]]
@@ -1707,30 +1735,32 @@ wheels = [
[[package]]
name = "packaging"
-version = "24.2"
+version = "25.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" },
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pandas"
-version = "1.5.3"
+version = "2.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
+ { name = "tzdata" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/74/ee/146cab1ff6d575b54ace8a6a5994048380dc94879b0125b25e62edcb9e52/pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1", size = 5203060, upload-time = "2023-01-19T08:31:39.615Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a9/cd/34f6b0780301be81be804d7aa71d571457369e6131e2b330af2b0fed1aad/pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406", size = 18619230, upload-time = "2023-01-19T08:29:07.301Z" },
- { url = "https://files.pythonhosted.org/packages/5f/34/b7858bb7d6d6bf4d9df1dde777a11fcf3ff370e1d1b3956e3d0fcca8322c/pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572", size = 11982991, upload-time = "2023-01-19T08:29:15.383Z" },
- { url = "https://files.pythonhosted.org/packages/b8/6c/005bd604994f7cbede4d7bf030614ef49a2213f76bc3d738ecf5b0dcc810/pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996", size = 10927131, upload-time = "2023-01-19T08:29:20.342Z" },
- { url = "https://files.pythonhosted.org/packages/27/c7/35b81ce5f680f2dac55eac14d103245cd8cf656ae4a2ff3be2e69fd1d330/pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354", size = 11368188, upload-time = "2023-01-19T08:29:25.807Z" },
- { url = "https://files.pythonhosted.org/packages/49/e2/79e46612dc25ebc7603dc11c560baa7266c90f9e48537ecf1a02a0dd6bff/pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23", size = 12062104, upload-time = "2023-01-19T08:29:30.695Z" },
- { url = "https://files.pythonhosted.org/packages/d9/cd/f27c2992cbe05a3e39937f73a4be635a9ec149ec3ca4467d8cf039718994/pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328", size = 10362473, upload-time = "2023-01-19T08:29:37.506Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" },
+ { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" },
+ { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" },
+ { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" },
]
[[package]]
@@ -1774,11 +1804,11 @@ wheels = [
[[package]]
name = "platformdirs"
-version = "4.3.8"
+version = "4.4.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
+ { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
]
[[package]]
@@ -1817,11 +1847,11 @@ wheels = [
[[package]]
name = "protego"
-version = "0.4.0"
+version = "0.5.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4e/6b/84e878d0567dfc11538bad6ce2595cee7ae0c47cf6bf7293683c9ec78ef8/protego-0.4.0.tar.gz", hash = "sha256:93a5e662b61399a0e1f208a324f2c6ea95b23ee39e6cbf2c96246da4a656c2f6", size = 3246425, upload-time = "2025-01-17T15:48:21.644Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/9b/9c3a649167c7e43a0818df515d515e66d95a261fdfdf2a6afd45be9db696/protego-0.5.0.tar.gz", hash = "sha256:225dee0acfcc71de8c6f7cef9c618e5a9d3e7baa7ae1470b8d076a064033c463", size = 3137494, upload-time = "2025-06-24T13:58:45.31Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d9/fd/8d84d75832b0983cecf3aff7ae48362fe96fc8ab6ebca9dcf3cefd87e79c/Protego-0.4.0-py2.py3-none-any.whl", hash = "sha256:37640bc0ebe37572d624453a21381d05e9d86e44f89ff1e81794d185a0491666", size = 8553, upload-time = "2025-01-17T15:48:18.332Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/cb/4347985f89ca3e4beb5d0cb85f8b951c9e339564bd2a3f388d6fb78382cc/protego-0.5.0-py3-none-any.whl", hash = "sha256:4237227840a67fdeec289a9b89652455b5657806388c17e1a556e160435f8fc5", size = 10356, upload-time = "2025-06-24T13:58:44.08Z" },
]
[[package]]
@@ -1866,19 +1896,17 @@ wheels = [
[[package]]
name = "pyarrow"
-version = "20.0.0"
+version = "21.0.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/ee/a7810cb9f3d6e9238e61d312076a9859bf3668fd21c69744de9532383912/pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1", size = 1125187, upload-time = "2025-04-27T12:34:23.264Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5b/23/77094eb8ee0dbe88441689cb6afc40ac312a1e15d3a7acc0586999518222/pyarrow-20.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:c7dd06fd7d7b410ca5dc839cc9d485d2bc4ae5240851bcd45d85105cc90a47d7", size = 30832591, upload-time = "2025-04-27T12:27:27.89Z" },
- { url = "https://files.pythonhosted.org/packages/c3/d5/48cc573aff00d62913701d9fac478518f693b30c25f2c157550b0b2565cb/pyarrow-20.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d5382de8dc34c943249b01c19110783d0d64b207167c728461add1ecc2db88e4", size = 32273686, upload-time = "2025-04-27T12:27:36.816Z" },
- { url = "https://files.pythonhosted.org/packages/37/df/4099b69a432b5cb412dd18adc2629975544d656df3d7fda6d73c5dba935d/pyarrow-20.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6415a0d0174487456ddc9beaead703d0ded5966129fa4fd3114d76b5d1c5ceae", size = 41337051, upload-time = "2025-04-27T12:27:44.4Z" },
- { url = "https://files.pythonhosted.org/packages/4c/27/99922a9ac1c9226f346e3a1e15e63dee6f623ed757ff2893f9d6994a69d3/pyarrow-20.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15aa1b3b2587e74328a730457068dc6c89e6dcbf438d4369f572af9d320a25ee", size = 42404659, upload-time = "2025-04-27T12:27:51.715Z" },
- { url = "https://files.pythonhosted.org/packages/21/d1/71d91b2791b829c9e98f1e0d85be66ed93aff399f80abb99678511847eaa/pyarrow-20.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5605919fbe67a7948c1f03b9f3727d82846c053cd2ce9303ace791855923fd20", size = 40695446, upload-time = "2025-04-27T12:27:59.643Z" },
- { url = "https://files.pythonhosted.org/packages/f1/ca/ae10fba419a6e94329707487835ec721f5a95f3ac9168500bcf7aa3813c7/pyarrow-20.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a5704f29a74b81673d266e5ec1fe376f060627c2e42c5c7651288ed4b0db29e9", size = 42278528, upload-time = "2025-04-27T12:28:07.297Z" },
- { url = "https://files.pythonhosted.org/packages/7a/a6/aba40a2bf01b5d00cf9cd16d427a5da1fad0fb69b514ce8c8292ab80e968/pyarrow-20.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:00138f79ee1b5aca81e2bdedb91e3739b987245e11fa3c826f9e57c5d102fb75", size = 42918162, upload-time = "2025-04-27T12:28:15.716Z" },
- { url = "https://files.pythonhosted.org/packages/93/6b/98b39650cd64f32bf2ec6d627a9bd24fcb3e4e6ea1873c5e1ea8a83b1a18/pyarrow-20.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f2d67ac28f57a362f1a2c1e6fa98bfe2f03230f7e15927aecd067433b1e70ce8", size = 44550319, upload-time = "2025-04-27T12:28:27.026Z" },
- { url = "https://files.pythonhosted.org/packages/ab/32/340238be1eb5037e7b5de7e640ee22334417239bc347eadefaf8c373936d/pyarrow-20.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:4a8b029a07956b8d7bd742ffca25374dd3f634b35e46cc7a7c3fa4c75b297191", size = 25770759, upload-time = "2025-04-27T12:28:33.702Z" },
+ { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837, upload-time = "2025-07-18T00:54:34.755Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470, upload-time = "2025-07-18T00:54:38.329Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619, upload-time = "2025-07-18T00:54:42.172Z" },
+ { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488, upload-time = "2025-07-18T00:54:47.132Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159, upload-time = "2025-07-18T00:54:51.686Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567, upload-time = "2025-07-18T00:54:56.679Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959, upload-time = "2025-07-18T00:55:00.482Z" },
]
[[package]]
@@ -1904,16 +1932,16 @@ wheels = [
[[package]]
name = "pycparser"
-version = "2.22"
+version = "2.23"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "pydantic"
-version = "2.11.7"
+version = "2.11.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -1921,9 +1949,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" },
]
[[package]]
@@ -1973,16 +2001,16 @@ wheels = [
[[package]]
name = "pydantic-settings"
-version = "2.9.1"
+version = "2.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" },
]
[[package]]
@@ -2040,11 +2068,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/6d/a8/10cf6b955b5fa1943
[[package]]
name = "pyparsing"
-version = "3.2.3"
+version = "3.2.5"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
+ { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
]
[[package]]
@@ -2071,7 +2099,7 @@ wheels = [
[[package]]
name = "pytest"
-version = "8.4.1"
+version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -2082,22 +2110,23 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "pytest-asyncio"
-version = "1.1.0"
+version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-asyncio-runner" },
{ name = "pytest" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" },
+ { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" },
]
[[package]]
@@ -2114,11 +2143,11 @@ wheels = [
[[package]]
name = "python-dotenv"
-version = "1.1.0"
+version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
]
[[package]]
@@ -2154,6 +2183,16 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
+[[package]]
+name = "pywin32"
+version = "311"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" },
+ { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" },
+]
+
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -2202,44 +2241,43 @@ wheels = [
[[package]]
name = "regex"
-version = "2024.11.6"
+version = "2025.9.18"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/d3/eaa0d28aba6ad1827ad1e716d9a93e1ba963ada61887498297d3da715133/regex-2025.9.18.tar.gz", hash = "sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4", size = 400917, upload-time = "2025-09-19T00:38:35.79Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674, upload-time = "2024-11-06T20:08:57.575Z" },
- { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684, upload-time = "2024-11-06T20:08:59.787Z" },
- { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589, upload-time = "2024-11-06T20:09:01.896Z" },
- { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511, upload-time = "2024-11-06T20:09:04.062Z" },
- { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149, upload-time = "2024-11-06T20:09:06.237Z" },
- { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707, upload-time = "2024-11-06T20:09:07.715Z" },
- { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702, upload-time = "2024-11-06T20:09:10.101Z" },
- { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976, upload-time = "2024-11-06T20:09:11.566Z" },
- { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397, upload-time = "2024-11-06T20:09:13.119Z" },
- { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726, upload-time = "2024-11-06T20:09:14.85Z" },
- { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098, upload-time = "2024-11-06T20:09:16.504Z" },
- { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325, upload-time = "2024-11-06T20:09:18.698Z" },
- { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277, upload-time = "2024-11-06T20:09:21.725Z" },
- { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197, upload-time = "2024-11-06T20:09:24.092Z" },
- { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714, upload-time = "2024-11-06T20:09:26.36Z" },
- { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042, upload-time = "2024-11-06T20:09:28.762Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/d8/7e06171db8e55f917c5b8e89319cea2d86982e3fc46b677f40358223dece/regex-2025.9.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:12296202480c201c98a84aecc4d210592b2f55e200a1d193235c4db92b9f6788", size = 484829, upload-time = "2025-09-19T00:35:05.215Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/70/bf91bb39e5bedf75ce730ffbaa82ca585584d13335306d637458946b8b9f/regex-2025.9.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:220381f1464a581f2ea988f2220cf2a67927adcef107d47d6897ba5a2f6d51a4", size = 288993, upload-time = "2025-09-19T00:35:08.154Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/89/69f79b28365eda2c46e64c39d617d5f65a2aa451a4c94de7d9b34c2dc80f/regex-2025.9.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87f681bfca84ebd265278b5daa1dcb57f4db315da3b5d044add7c30c10442e61", size = 286624, upload-time = "2025-09-19T00:35:09.717Z" },
+ { url = "https://files.pythonhosted.org/packages/44/31/81e62955726c3a14fcc1049a80bc716765af6c055706869de5e880ddc783/regex-2025.9.18-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34d674cbba70c9398074c8a1fcc1a79739d65d1105de2a3c695e2b05ea728251", size = 780473, upload-time = "2025-09-19T00:35:11.013Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/23/07072b7e191fbb6e213dc03b2f5b96f06d3c12d7deaded84679482926fc7/regex-2025.9.18-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:385c9b769655cb65ea40b6eea6ff763cbb6d69b3ffef0b0db8208e1833d4e746", size = 849290, upload-time = "2025-09-19T00:35:12.348Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/f0/aec7f6a01f2a112210424d77c6401b9015675fb887ced7e18926df4ae51e/regex-2025.9.18-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8900b3208e022570ae34328712bef6696de0804c122933414014bae791437ab2", size = 897335, upload-time = "2025-09-19T00:35:14.058Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/90/2e5f9da89d260de7d0417ead91a1bc897f19f0af05f4f9323313b76c47f2/regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c204e93bf32cd7a77151d44b05eb36f469d0898e3fba141c026a26b79d9914a0", size = 789946, upload-time = "2025-09-19T00:35:15.403Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/d5/1c712c7362f2563d389be66bae131c8bab121a3fabfa04b0b5bfc9e73c51/regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3acc471d1dd7e5ff82e6cacb3b286750decd949ecd4ae258696d04f019817ef8", size = 780787, upload-time = "2025-09-19T00:35:17.061Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/92/c54cdb4aa41009632e69817a5aa452673507f07e341076735a2f6c46a37c/regex-2025.9.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6479d5555122433728760e5f29edb4c2b79655a8deb681a141beb5c8a025baea", size = 773632, upload-time = "2025-09-19T00:35:18.57Z" },
+ { url = "https://files.pythonhosted.org/packages/db/99/75c996dc6a2231a8652d7ad0bfbeaf8a8c77612d335580f520f3ec40e30b/regex-2025.9.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:431bd2a8726b000eb6f12429c9b438a24062a535d06783a93d2bcbad3698f8a8", size = 844104, upload-time = "2025-09-19T00:35:20.259Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/f7/25aba34cc130cb6844047dbfe9716c9b8f9629fee8b8bec331aa9241b97b/regex-2025.9.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0cc3521060162d02bd36927e20690129200e5ac9d2c6d32b70368870b122db25", size = 834794, upload-time = "2025-09-19T00:35:22.002Z" },
+ { url = "https://files.pythonhosted.org/packages/51/eb/64e671beafa0ae29712268421597596d781704973551312b2425831d4037/regex-2025.9.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a021217b01be2d51632ce056d7a837d3fa37c543ede36e39d14063176a26ae29", size = 778535, upload-time = "2025-09-19T00:35:23.298Z" },
+ { url = "https://files.pythonhosted.org/packages/26/33/c0ebc0b07bd0bf88f716cca240546b26235a07710ea58e271cfe390ae273/regex-2025.9.18-cp310-cp310-win32.whl", hash = "sha256:4a12a06c268a629cb67cc1d009b7bb0be43e289d00d5111f86a2efd3b1949444", size = 264115, upload-time = "2025-09-19T00:35:25.206Z" },
+ { url = "https://files.pythonhosted.org/packages/59/39/aeb11a4ae68faaec2498512cadae09f2d8a91f1f65730fe62b9bffeea150/regex-2025.9.18-cp310-cp310-win_amd64.whl", hash = "sha256:47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450", size = 276143, upload-time = "2025-09-19T00:35:26.785Z" },
+ { url = "https://files.pythonhosted.org/packages/29/04/37f2d3fc334a1031fc2767c9d89cec13c2e72207c7e7f6feae8a47f4e149/regex-2025.9.18-cp310-cp310-win_arm64.whl", hash = "sha256:16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442", size = 268473, upload-time = "2025-09-19T00:35:28.39Z" },
]
[[package]]
name = "reportlab"
-version = "4.4.2"
+version = "4.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "charset-normalizer" },
{ name = "pillow" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ec/9b/3483c7e4ad33d15f22d528872439e5bc92485814d7e7d10dbc3130368a83/reportlab-4.4.2.tar.gz", hash = "sha256:fc6283048ddd0781a9db1d671715990e6aa059c8d40ec9baf34294c4bd583a36", size = 3509063, upload-time = "2025-06-18T12:20:19.526Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/fa/ed71f3e750afb77497641eb0194aeda069e271ce6d6931140f8787e0e69a/reportlab-4.4.4.tar.gz", hash = "sha256:cb2f658b7f4a15be2cc68f7203aa67faef67213edd4f2d4bdd3eb20dab75a80d", size = 3711935, upload-time = "2025-09-19T10:43:36.502Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/74/ed990bc9586605d4e46f6b0e0b978a5b8e757aa599e39664bee26d6dc666/reportlab-4.4.2-py3-none-any.whl", hash = "sha256:58e11be387457928707c12153b7e41e52533a5da3f587b15ba8f8fd0805c6ee2", size = 1953624, upload-time = "2025-06-18T12:20:16.152Z" },
+ { url = "https://files.pythonhosted.org/packages/57/66/e040586fe6f9ae7f3a6986186653791fb865947f0b745290ee4ab026b834/reportlab-4.4.4-py3-none-any.whl", hash = "sha256:299b3b0534e7202bb94ed2ddcd7179b818dcda7de9d8518a57c85a58a1ebaadb", size = 1954981, upload-time = "2025-09-19T10:43:33.589Z" },
]
[[package]]
name = "requests"
-version = "2.32.4"
+version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -2247,9 +2285,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
@@ -2267,35 +2305,37 @@ wheels = [
[[package]]
name = "rpds-py"
-version = "0.25.1"
+version = "0.27.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8c/a6/60184b7fc00dd3ca80ac635dd5b8577d444c57e8e8742cecabfacb829921/rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3", size = 27304, upload-time = "2025-05-21T12:46:12.502Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/09/e1158988e50905b7f8306487a576b52d32aa9a87f79f7ab24ee8db8b6c05/rpds_py-0.25.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f4ad628b5174d5315761b67f212774a32f5bad5e61396d38108bd801c0a8f5d9", size = 373140, upload-time = "2025-05-21T12:42:38.834Z" },
- { url = "https://files.pythonhosted.org/packages/e0/4b/a284321fb3c45c02fc74187171504702b2934bfe16abab89713eedfe672e/rpds_py-0.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c742af695f7525e559c16f1562cf2323db0e3f0fbdcabdf6865b095256b2d40", size = 358860, upload-time = "2025-05-21T12:42:41.394Z" },
- { url = "https://files.pythonhosted.org/packages/4e/46/8ac9811150c75edeae9fc6fa0e70376c19bc80f8e1f7716981433905912b/rpds_py-0.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:605ffe7769e24b1800b4d024d24034405d9404f0bc2f55b6db3362cd34145a6f", size = 386179, upload-time = "2025-05-21T12:42:43.213Z" },
- { url = "https://files.pythonhosted.org/packages/f3/ec/87eb42d83e859bce91dcf763eb9f2ab117142a49c9c3d17285440edb5b69/rpds_py-0.25.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc6f3ddef93243538be76f8e47045b4aad7a66a212cd3a0f23e34469473d36b", size = 400282, upload-time = "2025-05-21T12:42:44.92Z" },
- { url = "https://files.pythonhosted.org/packages/68/c8/2a38e0707d7919c8c78e1d582ab15cf1255b380bcb086ca265b73ed6db23/rpds_py-0.25.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f70316f760174ca04492b5ab01be631a8ae30cadab1d1081035136ba12738cfa", size = 521824, upload-time = "2025-05-21T12:42:46.856Z" },
- { url = "https://files.pythonhosted.org/packages/5e/2c/6a92790243569784dde84d144bfd12bd45102f4a1c897d76375076d730ab/rpds_py-0.25.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1dafef8df605fdb46edcc0bf1573dea0d6d7b01ba87f85cd04dc855b2b4479e", size = 411644, upload-time = "2025-05-21T12:42:48.838Z" },
- { url = "https://files.pythonhosted.org/packages/eb/76/66b523ffc84cf47db56efe13ae7cf368dee2bacdec9d89b9baca5e2e6301/rpds_py-0.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0701942049095741a8aeb298a31b203e735d1c61f4423511d2b1a41dcd8a16da", size = 386955, upload-time = "2025-05-21T12:42:50.835Z" },
- { url = "https://files.pythonhosted.org/packages/b6/b9/a362d7522feaa24dc2b79847c6175daa1c642817f4a19dcd5c91d3e2c316/rpds_py-0.25.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e87798852ae0b37c88babb7f7bbbb3e3fecc562a1c340195b44c7e24d403e380", size = 421039, upload-time = "2025-05-21T12:42:52.348Z" },
- { url = "https://files.pythonhosted.org/packages/0f/c4/b5b6f70b4d719b6584716889fd3413102acf9729540ee76708d56a76fa97/rpds_py-0.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3bcce0edc1488906c2d4c75c94c70a0417e83920dd4c88fec1078c94843a6ce9", size = 563290, upload-time = "2025-05-21T12:42:54.404Z" },
- { url = "https://files.pythonhosted.org/packages/87/a3/2e6e816615c12a8f8662c9d8583a12eb54c52557521ef218cbe3095a8afa/rpds_py-0.25.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e2f6a2347d3440ae789505693a02836383426249d5293541cd712e07e7aecf54", size = 592089, upload-time = "2025-05-21T12:42:55.976Z" },
- { url = "https://files.pythonhosted.org/packages/c0/08/9b8e1050e36ce266135994e2c7ec06e1841f1c64da739daeb8afe9cb77a4/rpds_py-0.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4fd52d3455a0aa997734f3835cbc4c9f32571345143960e7d7ebfe7b5fbfa3b2", size = 558400, upload-time = "2025-05-21T12:42:58.032Z" },
- { url = "https://files.pythonhosted.org/packages/f2/df/b40b8215560b8584baccd839ff5c1056f3c57120d79ac41bd26df196da7e/rpds_py-0.25.1-cp310-cp310-win32.whl", hash = "sha256:3f0b1798cae2bbbc9b9db44ee068c556d4737911ad53a4e5093d09d04b3bbc24", size = 219741, upload-time = "2025-05-21T12:42:59.479Z" },
- { url = "https://files.pythonhosted.org/packages/10/99/e4c58be18cf5d8b40b8acb4122bc895486230b08f978831b16a3916bd24d/rpds_py-0.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3ebd879ab996537fc510a2be58c59915b5dd63bccb06d1ef514fee787e05984a", size = 231553, upload-time = "2025-05-21T12:43:01.425Z" },
- { url = "https://files.pythonhosted.org/packages/78/ff/566ce53529b12b4f10c0a348d316bd766970b7060b4fd50f888be3b3b281/rpds_py-0.25.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b24bf3cd93d5b6ecfbedec73b15f143596c88ee249fa98cefa9a9dc9d92c6f28", size = 373931, upload-time = "2025-05-21T12:45:05.01Z" },
- { url = "https://files.pythonhosted.org/packages/83/5d/deba18503f7c7878e26aa696e97f051175788e19d5336b3b0e76d3ef9256/rpds_py-0.25.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0eb90e94f43e5085623932b68840b6f379f26db7b5c2e6bcef3179bd83c9330f", size = 359074, upload-time = "2025-05-21T12:45:06.714Z" },
- { url = "https://files.pythonhosted.org/packages/0d/74/313415c5627644eb114df49c56a27edba4d40cfd7c92bd90212b3604ca84/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d50e4864498a9ab639d6d8854b25e80642bd362ff104312d9770b05d66e5fb13", size = 387255, upload-time = "2025-05-21T12:45:08.669Z" },
- { url = "https://files.pythonhosted.org/packages/8c/c8/c723298ed6338963d94e05c0f12793acc9b91d04ed7c4ba7508e534b7385/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c9409b47ba0650544b0bb3c188243b83654dfe55dcc173a86832314e1a6a35d", size = 400714, upload-time = "2025-05-21T12:45:10.39Z" },
- { url = "https://files.pythonhosted.org/packages/33/8a/51f1f6aa653c2e110ed482ef2ae94140d56c910378752a1b483af11019ee/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:796ad874c89127c91970652a4ee8b00d56368b7e00d3477f4415fe78164c8000", size = 523105, upload-time = "2025-05-21T12:45:12.273Z" },
- { url = "https://files.pythonhosted.org/packages/c7/a4/7873d15c088ad3bff36910b29ceb0f178e4b3232c2adbe9198de68a41e63/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85608eb70a659bf4c1142b2781083d4b7c0c4e2c90eff11856a9754e965b2540", size = 411499, upload-time = "2025-05-21T12:45:13.95Z" },
- { url = "https://files.pythonhosted.org/packages/90/f3/0ce1437befe1410766d11d08239333ac1b2d940f8a64234ce48a7714669c/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4feb9211d15d9160bc85fa72fed46432cdc143eb9cf6d5ca377335a921ac37b", size = 387918, upload-time = "2025-05-21T12:45:15.649Z" },
- { url = "https://files.pythonhosted.org/packages/94/d4/5551247988b2a3566afb8a9dba3f1d4a3eea47793fd83000276c1a6c726e/rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ccfa689b9246c48947d31dd9d8b16d89a0ecc8e0e26ea5253068efb6c542b76e", size = 421705, upload-time = "2025-05-21T12:45:17.788Z" },
- { url = "https://files.pythonhosted.org/packages/b0/25/5960f28f847bf736cc7ee3c545a7e1d2f3b5edaf82c96fb616c2f5ed52d0/rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3c5b317ecbd8226887994852e85de562f7177add602514d4ac40f87de3ae45a8", size = 564489, upload-time = "2025-05-21T12:45:19.466Z" },
- { url = "https://files.pythonhosted.org/packages/02/66/1c99884a0d44e8c2904d3c4ec302f995292d5dde892c3bf7685ac1930146/rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:454601988aab2c6e8fd49e7634c65476b2b919647626208e376afcd22019eeb8", size = 592557, upload-time = "2025-05-21T12:45:21.362Z" },
- { url = "https://files.pythonhosted.org/packages/55/ae/4aeac84ebeffeac14abb05b3bb1d2f728d00adb55d3fb7b51c9fa772e760/rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1c0c434a53714358532d13539272db75a5ed9df75a4a090a753ac7173ec14e11", size = 558691, upload-time = "2025-05-21T12:45:23.084Z" },
- { url = "https://files.pythonhosted.org/packages/41/b3/728a08ff6f5e06fe3bb9af2e770e9d5fd20141af45cff8dfc62da4b2d0b3/rpds_py-0.25.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f73ce1512e04fbe2bc97836e89830d6b4314c171587a99688082d090f934d20a", size = 231651, upload-time = "2025-05-21T12:45:24.72Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" },
+ { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" },
+ { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" },
+ { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" },
+ { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" },
+ { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" },
+ { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" },
+ { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" },
+ { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" },
]
[[package]]
@@ -2312,19 +2352,19 @@ wheels = [
[[package]]
name = "s3transfer"
-version = "0.13.1"
+version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589, upload-time = "2025-07-18T19:22:42.31Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308, upload-time = "2025-07-18T19:22:40.947Z" },
+ { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" },
]
[[package]]
name = "scenedetect"
-version = "0.6.6"
+version = "0.6.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -2332,9 +2372,9 @@ dependencies = [
{ name = "platformdirs" },
{ name = "tqdm" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/59/36/1e29ac958e2d2b5e4365fb7de03f94a98b9949c46267e682bcfe22460812/scenedetect-0.6.6.tar.gz", hash = "sha256:4b50946abca886bd623e7a304e30da197f0e7e69cd65d80115d551538261c35b", size = 165791, upload-time = "2025-03-10T01:40:57.693Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/e77e1812ae89bc4864ff54efb6a8232eabebd471e372096cb711f03cca52/scenedetect-0.6.7.1.tar.gz", hash = "sha256:07833b0cb83a0106786a88136462580e9865e097f411f01501a688714c483a4e", size = 164208, upload-time = "2025-09-25T03:18:59.929Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b1/5b/c090fe55521265eb1816c303267e985125000a4e32237e95562ed462608f/scenedetect-0.6.6-py3-none-any.whl", hash = "sha256:cbd47e4aff1d3ba6f4ee00e54ff9af26378aa2b48f501003dddf5f96e37d3eb0", size = 131581, upload-time = "2025-03-10T01:40:56.136Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5b/2294ea44b3b2264a50b82a55a3822837a04cb779c873fc168844f36f5b46/scenedetect-0.6.7.1-py3-none-any.whl", hash = "sha256:3808ef4436ab0fc6fee8a155e95759b42e2446c925c45746a82ff119be4eb3e1", size = 130860, upload-time = "2025-09-25T03:18:58.43Z" },
]
[[package]]
@@ -2381,11 +2421,11 @@ wheels = [
[[package]]
name = "soupsieve"
-version = "2.7"
+version = "2.8"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
]
[[package]]
@@ -2402,26 +2442,27 @@ wheels = [
[[package]]
name = "sse-starlette"
-version = "2.3.6"
+version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284, upload-time = "2025-05-30T13:34:12.914Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606, upload-time = "2025-05-30T13:34:11.703Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" },
]
[[package]]
name = "starlette"
-version = "0.46.2"
+version = "0.48.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
+ { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
]
[[package]]
@@ -2487,7 +2528,7 @@ wheels = [
[[package]]
name = "traceroot"
-version = "0.0.4a10"
+version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -2508,18 +2549,18 @@ dependencies = [
{ name = "pyyaml" },
{ name = "watchtower" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c6/11/2775fbdf994511ce08c4b99b2493137629075f6e2b0c6242be7045966ae7/traceroot-0.0.4a10.tar.gz", hash = "sha256:1b32f91091d3f81a994d95f7cf7295d22a0b7980012a95f06a04e23d0cadee3a", size = 23623, upload-time = "2025-08-09T21:33:01.136Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/5e/8ade61cadecf69b4fa49205640a7424880bc25b5e9615159ba8cf4aff2bf/traceroot-0.0.5.tar.gz", hash = "sha256:0924d9b524a9e59d64c4eec4c812018f2d7583558de17001294ace96874381c0", size = 28066, upload-time = "2025-08-24T03:29:04.966Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f4/77/98f46097a4b1b92452b0da8823fbe8e7e7d139dab7eb4192db34f2e11283/traceroot-0.0.4a10-py3-none-any.whl", hash = "sha256:63647d1bbd094e7c2adc7cd45b9aae07198b0d7051a9ef6e682bd88029a49949", size = 20249, upload-time = "2025-08-09T21:32:59.558Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2777d7c3d0e36b3b2d9f903151991f3e1c21f190788c0e5f537fd762ae34/traceroot-0.0.5-py3-none-any.whl", hash = "sha256:ec27afb4ac33df3109c4c436f3bdfc47e30e2f1ce5eb90ba2215cfebe19e6b2e", size = 24324, upload-time = "2025-08-24T03:29:03.446Z" },
]
[[package]]
name = "typing-extensions"
-version = "4.14.0"
+version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
@@ -2534,6 +2575,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
]
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]
+
[[package]]
name = "uritemplate"
version = "4.2.0"
@@ -2554,16 +2604,16 @@ wheels = [
[[package]]
name = "uvicorn"
-version = "0.34.3"
+version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
+ { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" },
]
[package.optional-dependencies]
@@ -2746,11 +2796,11 @@ wheels = [
[[package]]
name = "xlsxwriter"
-version = "3.2.5"
+version = "3.2.9"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a7/47/7704bac42ac6fe1710ae099b70e6a1e68ed173ef14792b647808c357da43/xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe", size = 213306, upload-time = "2025-06-17T08:59:14.619Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fa/34/a22e6664211f0c8879521328000bdcae9bf6dbafa94a923e531f6d5b3f73/xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd", size = 172347, upload-time = "2025-06-17T08:59:13.453Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" },
]
[[package]]
diff --git a/electron/main/fileReader.ts b/electron/main/fileReader.ts
index 07201cdc8..99bb22198 100644
--- a/electron/main/fileReader.ts
+++ b/electron/main/fileReader.ts
@@ -445,10 +445,10 @@ export class FileReader {
if (type === 'md') {
const content = fs.readFileSync(filePath, 'utf-8')
resolve(content)
- } else if (isShowSourceCode && type === 'html') {
+ } else if (type === 'html') {
const content = fs.readFileSync(filePath, 'utf-8')
resolve(content)
- } else if (["pdf", "html"].includes(type)) {
+ } else if (["pdf"].includes(type)) {
resolve(filePath)
} else if (type === "csv") {
try {
diff --git a/electron/main/index.ts b/electron/main/index.ts
index 9d87c76ac..733b8b67d 100644
--- a/electron/main/index.ts
+++ b/electron/main/index.ts
@@ -4,7 +4,7 @@ import path from 'node:path'
import os, { homedir } from 'node:os'
import log from 'electron-log'
import { update, registerUpdateIpcHandlers } from './update'
-import { checkToolInstalled, installDependencies, killProcessOnPort, startBackend } from './init'
+import { checkToolInstalled, killProcessOnPort, startBackend } from './init'
import { WebViewManager } from './webview'
import { FileReader } from './fileReader'
import { ChildProcessWithoutNullStreams } from 'node:child_process'
@@ -18,9 +18,10 @@ import kill from 'tree-kill';
import { zipFolder } from './utils/log'
import axios from 'axios';
import FormData from 'form-data';
+import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps'
+import { isBinaryExists, getBackendPath, getVenvPath } from './utils/process'
const userData = app.getPath('userData');
-const versionFile = path.join(userData, 'version.txt');
// ==================== constants ====================
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -50,69 +51,6 @@ findAvailablePort(browser_port).then(port => {
app.commandLine.appendSwitch('remote-debugging-port', port + '');
});
-// Read last run version and install dependencies on update
-async function checkAndInstallDepsOnUpdate(): Promise {
- const currentVersion = app.getVersion();
- return new Promise(async (resolve, reject) => {
- try {
- log.info(' start check version', { currentVersion });
-
- // Check if version file exists
- const versionExists = fs.existsSync(versionFile);
- let savedVersion = '';
-
- if (versionExists) {
- savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
- log.info(' read saved version', { savedVersion });
- } else {
- log.info(' version file not exist, will create new file');
- }
-
- // If version file does not exist or version does not match, reinstall dependencies
- if (!versionExists || savedVersion !== currentVersion) {
- log.info(' version changed, prepare to reinstall uv dependencies...', {
- currentVersion,
- savedVersion: versionExists ? savedVersion : 'none',
- reason: !versionExists ? 'version file not exist' : 'version not match'
- });
-
- // Notify frontend to update
- if (win && !win.isDestroyed()) {
- win.webContents.send('update-notification', {
- type: 'version-update',
- currentVersion,
- previousVersion: versionExists ? savedVersion : 'none',
- reason: !versionExists ? 'version file not exist' : 'version not match'
- });
- }
-
- // Update version file
- fs.writeFileSync(versionFile, currentVersion);
- log.info(' version file updated', { currentVersion });
-
- // Install dependencies
- const result = await installDependencies();
- if (!result) {
- log.error(' install dependencies failed');
- resolve(false);
- return
- }
- resolve(true);
- log.info(' install dependencies complete');
- return
- } else {
- log.info(' version not changed, skip install dependencies', { currentVersion });
- resolve(true);
- return
- }
- } catch (error) {
- log.error(' check version and install dependencies error:', error);
- resolve(false);
- return
- }
- })
-}
-
// ==================== app config ====================
process.env.APP_ROOT = MAIN_DIST;
process.env.VITE_PUBLIC = VITE_PUBLIC;
@@ -259,51 +197,6 @@ const checkManagerInstance = (manager: any, name: string) => {
return manager;
};
-export const handleDependencyInstallation = async () => {
- try {
- log.info(' start install dependencies...');
-
- const isSuccess = await installDependencies();
- if (!isSuccess) {
- log.error(' install dependencies failed');
- return { success: false, error: 'install dependencies failed' };
- }
-
- log.info(' install dependencies success, check tool installed status...');
- const isToolInstalled = await checkToolInstalled();
- log.info('isToolInstalled && !python_process', isToolInstalled && !python_process);
- if (isToolInstalled && !python_process) {
- log.info(' tool installed, start backend service...');
- python_process = await startBackend((port) => {
- backendPort = port;
- log.info(' backend service start success', { port });
- });
-
- // Notify frontend to install success
- if (win && !win.isDestroyed()) {
- win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
- }
-
- python_process?.on('exit', (code, signal) => {
- log.info(' python process exit', { code, signal });
- });
- } else if (!isToolInstalled) {
- log.warn(' tool not installed, skip backend start');
- } else {
- log.info(' backend process already exist, skip start');
- }
-
- log.info(' install dependencies complete');
- return { success: true };
- } catch (error: any) {
- log.error(' install dependencies error:', error);
- if (win && !win.isDestroyed()) {
- win.webContents.send('install-dependencies-complete', { success: false, code: 2 });
- }
- return { success: false, error: error.message };
- }
-};
-
function registerIpcHandlers() {
// ==================== basic info handler ====================
ipcMain.handle('get-browser-port', () => {
@@ -934,13 +827,35 @@ function registerIpcHandlers() {
});
// ==================== dependency install handler ====================
- ipcMain.handle('install-dependencies', handleDependencyInstallation);
- ipcMain.handle('frontend-ready', handleDependencyInstallation);
+ ipcMain.handle('install-dependencies', async () => {
+ try {
+ if(win === null) throw new Error("Window is null");
+ //Force installation even if versionFile exists
+ const isInstalled = await checkAndInstallDepsOnUpdate({win, forceInstall: true});
+ return { success: true, isInstalled };
+ } catch (error) {
+ return { success: false, error: (error as Error).message };
+ }
+ });
ipcMain.handle('check-tool-installed', async () => {
try {
const isInstalled = await checkToolInstalled();
- return { success: true, isInstalled };
+ return { success: true, isInstalled: isInstalled.success };
+ } catch (error) {
+ return { success: false, error: (error as Error).message };
+ }
+ });
+
+ ipcMain.handle('get-installation-status', async () => {
+ try {
+ const { isInstalling, hasLockFile } = await getInstallationStatus();
+ return {
+ success: true,
+ isInstalling,
+ hasLockFile,
+ timestamp: Date.now()
+ };
} catch (error) {
return { success: false, error: (error as Error).message };
}
@@ -950,10 +865,34 @@ function registerIpcHandlers() {
registerUpdateIpcHandlers();
}
+// ==================== ensure eigent directories ====================
+const ensureEigentDirectories = () => {
+ const eigentBase = path.join(os.homedir(), '.eigent');
+ const requiredDirs = [
+ eigentBase,
+ path.join(eigentBase, 'bin'),
+ path.join(eigentBase, 'cache'),
+ path.join(eigentBase, 'venvs'),
+ path.join(eigentBase, 'runtime'),
+ ];
+
+ for (const dir of requiredDirs) {
+ if (!fs.existsSync(dir)) {
+ log.info(`Creating directory: ${dir}`);
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ }
+
+ log.info('.eigent directory structure ensured');
+};
+
// ==================== window create ====================
async function createWindow() {
const isMac = process.platform === 'darwin';
+ // Ensure .eigent directories exist before anything else
+ ensureEigentDirectories();
+
win = new BrowserWindow({
title: 'Eigent',
width: 1200,
@@ -988,14 +927,6 @@ async function createWindow() {
webViewManager.createWebview(i === 1 ? undefined : i.toString());
}
- // ==================== load content ====================
- if (VITE_DEV_SERVER_URL) {
- win.loadURL(VITE_DEV_SERVER_URL);
- win.webContents.openDevTools();
- } else {
- win.loadFile(indexHtml);
- }
-
// ==================== set event listeners ====================
setupWindowEventListeners();
setupDevToolsShortcuts();
@@ -1005,13 +936,175 @@ async function createWindow() {
// ==================== auto update ====================
update(win);
- // ==================== check tool installed ====================
- let res = await checkAndInstallDepsOnUpdate();
- if (!res) {
- log.info('checkAndInstallDepsOnUpdate,install dependencies failed');
- win.webContents.send('install-dependencies-complete', { success: false, code: 2 });
+ // ==================== CHECK IF INSTALLATION IS NEEDED BEFORE LOADING CONTENT ====================
+ log.info('Pre-checking if dependencies need to be installed...');
+
+ // Check version and tools status synchronously
+ const currentVersion = app.getVersion();
+ const versionFile = path.join(app.getPath('userData'), 'version.txt');
+ const versionExists = fs.existsSync(versionFile);
+ let savedVersion = '';
+ if (versionExists) {
+ savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
+ }
+
+ const uvExists = await isBinaryExists('uv');
+ const bunExists = await isBinaryExists('bun');
+
+ // Check if installation was previously completed
+ const backendPath = getBackendPath();
+ const installedLockPath = path.join(backendPath, 'uv_installed.lock');
+ const installationCompleted = fs.existsSync(installedLockPath);
+
+ // Check if venv path exists for current version
+ const venvPath = getVenvPath(currentVersion);
+ const venvExists = fs.existsSync(venvPath);
+
+ const needsInstallation = !versionExists || savedVersion !== currentVersion || !uvExists || !bunExists || !installationCompleted || !venvExists;
+
+ log.info('Installation check result:', {
+ needsInstallation,
+ versionExists,
+ versionMatch: savedVersion === currentVersion,
+ uvExists,
+ bunExists,
+ installationCompleted,
+ venvExists,
+ venvPath
+ });
+
+ // Handle localStorage based on installation state
+ if (needsInstallation) {
+ log.info('Installation needed - clearing auth storage to force carousel state');
+
+ // Clear the persisted auth storage file to force fresh initialization with carousel
+ const localStoragePath = path.join(app.getPath('userData'), 'Local Storage');
+ const leveldbPath = path.join(localStoragePath, 'leveldb');
+
+ try {
+ // Delete the localStorage database to force fresh init
+ if (fs.existsSync(leveldbPath)) {
+ log.info('Removing localStorage database to force fresh state...');
+ fs.rmSync(leveldbPath, { recursive: true, force: true });
+ log.info('Successfully cleared localStorage');
+ }
+ } catch (error) {
+ log.error('Error clearing localStorage:', error);
+ }
+
+ // Set up the injection for when page loads
+ win.webContents.once('dom-ready', () => {
+ if (!win || win.isDestroyed()) {
+ log.warn('Window destroyed before DOM ready - skipping localStorage injection');
+ return;
+ }
+ log.info('DOM ready - creating auth-storage with carousel state');
+ win.webContents.executeJavaScript(`
+ (function() {
+ try {
+ // Create fresh auth storage with carousel state
+ const newAuthStorage = {
+ state: {
+ token: null,
+ username: null,
+ email: null,
+ user_id: null,
+ appearance: 'light',
+ language: 'system',
+ isFirstLaunch: true,
+ modelType: 'cloud',
+ cloud_model_type: 'gpt-4.1',
+ initState: 'carousel',
+ share_token: null,
+ workerListData: {}
+ },
+ version: 0
+ };
+ localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
+ console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
+ } catch (e) {
+ console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e);
+ }
+ })();
+ `).catch(err => {
+ log.error('Failed to inject script:', err);
+ });
+ });
+ } else {
+ // Installation is complete - ensure initState is set to 'done'
+ log.info('Installation already complete - ensuring initState is done');
+
+ win.webContents.once('dom-ready', () => {
+ if (!win || win.isDestroyed()) {
+ log.warn('Window destroyed before DOM ready - skipping localStorage update');
+ return;
+ }
+ log.info('DOM ready - checking and updating auth-storage to done state');
+ win.webContents.executeJavaScript(`
+ (function() {
+ try {
+ const authStorage = localStorage.getItem('auth-storage');
+ if (authStorage) {
+ const parsed = JSON.parse(authStorage);
+ if (parsed.state && parsed.state.initState !== 'done') {
+ console.log('[ELECTRON] Updating initState from', parsed.state.initState, 'to done');
+ // Only update the initState field, preserve all other data
+ const updatedStorage = {
+ ...parsed,
+ state: {
+ ...parsed.state,
+ initState: 'done'
+ }
+ };
+ localStorage.setItem('auth-storage', JSON.stringify(updatedStorage));
+ console.log('[ELECTRON] initState updated to done, reloading page...');
+ return true; // Signal that we need to reload
+ }
+ }
+ return false; // No reload needed
+ } catch (e) {
+ console.error('[ELECTRON] Failed to update initState:', e);
+ // Don't modify localStorage if there's an error to prevent data corruption
+ return false;
+ }
+ })();
+ `).then(needsReload => {
+ if (needsReload) {
+ log.info('Reloading window after localStorage update');
+ win!.reload();
+ }
+ }).catch(err => {
+ log.error('Failed to inject script:', err);
+ });
+ });
+ }
+
+ // Load content
+ if (VITE_DEV_SERVER_URL) {
+ win.loadURL(VITE_DEV_SERVER_URL);
+ win.webContents.openDevTools();
+ } else {
+ win.loadFile(indexHtml);
+ }
+
+ // Wait for window to be ready
+ await new Promise(resolve => {
+ win!.webContents.once('did-finish-load', () => {
+ log.info('Window content loaded, starting dependency check immediately...');
+ resolve();
+ });
+ });
+
+ // Now check and install dependencies
+ let res:PromiseReturnType = await checkAndInstallDepsOnUpdate({ win });
+ if (!res.success) {
+ log.info("[DEPS INSTALL] Dependency Error: ", res.message);
+ win.webContents.send('install-dependencies-complete', { success: false, code: 2, error: res.message });
return;
}
+ log.info("[DEPS INSTALL] Dependency Success: ", res.message);
+
+ // Start backend after dependencies are ready
await checkAndStartBackend();
}
@@ -1069,27 +1162,30 @@ const setupExternalLinkHandling = () => {
// ==================== check and start backend ====================
const checkAndStartBackend = async () => {
log.info('Checking and starting backend service...');
+ try {
+ const isToolInstalled = await checkToolInstalled();
+ if (isToolInstalled.success) {
+ log.info('Tool installed, starting backend service...');
- const isToolInstalled = await checkToolInstalled();
- if (isToolInstalled) {
- log.info('Tool installed, starting backend service...');
+ // Notify frontend installation success
+ if (win && !win.isDestroyed()) {
+ win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
+ }
- // Notify frontend installation success
- if (win && !win.isDestroyed()) {
- win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
+ python_process = await startBackend((port) => {
+ backendPort = port;
+ log.info('Backend service started successfully', { port });
+ });
+
+ python_process?.on('exit', (code, signal) => {
+
+ log.info('Python process exited', { code, signal });
+ });
+ } else {
+ log.warn('Tool not installed, cannot start backend service');
}
-
- python_process = await startBackend((port) => {
- backendPort = port;
- log.info('Backend service started successfully', { port });
- });
-
- python_process?.on('exit', (code, signal) => {
-
- log.info('Python process exited', { code, signal });
- });
- } else {
- log.warn('Tool not installed, cannot start backend service');
+ } catch (error) {
+ log.debug("Cannot Start Backend due to ", error)
}
};
diff --git a/electron/main/init.ts b/electron/main/init.ts
index 1902fcad2..11ed6ef38 100644
--- a/electron/main/init.ts
+++ b/electron/main/init.ts
@@ -1,4 +1,4 @@
-import { getBackendPath, getBinaryPath, getCachePath, isBinaryExists, runInstallScript } from "./utils/process";
+import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, isBinaryExists, runInstallScript } from "./utils/process";
import { spawn, exec } from 'child_process'
import log from 'electron-log'
import fs from 'fs'
@@ -6,72 +6,30 @@ import path from 'path'
import * as net from "net";
import { ipcMain, BrowserWindow, app } from 'electron'
import { promisify } from 'util'
+import { detectInstallationLogs, PromiseReturnType } from "./install-deps";
const execAsync = promisify(exec);
// helper function to get main window
-function getMainWindow(): BrowserWindow | null {
+export function getMainWindow(): BrowserWindow | null {
const windows = BrowserWindow.getAllWindows();
return windows.length > 0 ? windows[0] : null;
}
export async function checkToolInstalled() {
- return new Promise(async (resolve, reject) => {
+ return new Promise(async (resolve, reject) => {
if (!(await isBinaryExists('uv'))) {
- resolve(false)
+ resolve({success: false, message: "uv doesn't exist"})
+ return
}
if (!(await isBinaryExists('bun'))) {
- resolve(false)
+ resolve({success: false, message: "Bun doesn't exist"})
+ return
}
- resolve(true)
- })
-
-}
-
-/**
- * Check if command line tools are installed, install if not
- */
-export async function installCommandTool() {
- return new Promise(async (resolve, reject) => {
- const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise => {
- if (await isBinaryExists(toolName)) {
- return true;
- }
-
- console.log(`start install ${toolName}`);
- await runInstallScript(scriptName);
- const installed = await isBinaryExists(toolName);
-
- const mainWindow = getMainWindow();
- if (mainWindow && !mainWindow.isDestroyed()) {
- if (installed) {
- mainWindow.webContents.send('install-dependencies-log', {
- type: 'stdout',
- data: `${toolName} installed successfully`,
- });
- } else {
- mainWindow.webContents.send('install-dependencies-complete', {
- success: false,
- code: 2,
- error: `${toolName} installation failed (script exit code 2)`,
- });
- }
- }
-
- return installed;
- };
-
- if (!(await ensureInstalled('uv', 'install-uv.js'))) {
- return reject("uv install failed");
- }
- if (!(await ensureInstalled('bun', 'install-bun.js'))) {
- return reject("bun install failed");
- }
-
- return resolve(true);
+ resolve({success: true, message: "Tools exist already"})
})
}
@@ -157,131 +115,16 @@ export async function installCommandTool() {
// })
// })
// }
-export async function installDependencies() {
- return new Promise(async (resolve, reject) => {
- console.log('start install dependencies')
-
- const mainWindow = getMainWindow();
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send('install-dependencies-start');
- }
-
- const isInstalCommandTool = await installCommandTool()
- if (!isInstalCommandTool) {
- resolve(false)
- return
- }
-
- const uv_path = await getBinaryPath('uv')
- const backendPath = getBackendPath()
-
- if (!fs.existsSync(backendPath)) {
- fs.mkdirSync(backendPath, { recursive: true })
- }
-
- const installingLockPath = path.join(backendPath, 'uv_installing.lock')
- fs.writeFileSync(installingLockPath, '')
-
- const installedLockPath = path.join(backendPath, 'uv_installed.lock')
- // const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple']
- const proxyArgs = ['--default-index', 'https://mirrors.aliyun.com/pypi/simple/']
- const runInstall = (extraArgs: string[]) => {
- return new Promise((resolveInner, rejectInner) => {
- try {
- const node_process = spawn(uv_path, [
- 'sync',
- '--no-dev',
- '--cache-dir', getCachePath('uv_cache'),
- ...extraArgs], {
- cwd: backendPath,
- env: {
- ...process.env,
- UV_TOOL_DIR: getCachePath('uv_tool'),
- UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
- }
- })
- console.log('start install dependencies', extraArgs)
- node_process.stdout.on('data', (data) => {
-
- log.info(`Script output: ${data}`)
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send('install-dependencies-log', { type: 'stdout', data: data.toString() });
- }
- })
-
- node_process.stderr.on('data', (data) => {
- log.error(`Script error: ${data}`)
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send('install-dependencies-log', { type: 'stderr', data: data.toString() });
- }
- })
-
- node_process.on('close', (code) => {
- console.log('install dependencies end', code === 0)
- resolveInner(code === 0)
- })
- }catch(err) {
- log.error('run install failed', err)
- // Clean up uv_installing.lock file if installation fails
- if (fs.existsSync(installingLockPath)) {
- fs.unlinkSync(installingLockPath);
- }
- rejectInner(err)
- }
-
- })
- }
-
- // try default install
- const installSuccess = await runInstall([])
-
- if (installSuccess) {
- fs.unlinkSync(installingLockPath)
- fs.writeFileSync(installedLockPath, '')
- log.info('Script completed successfully')
- console.log('end install dependencies')
- spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath })
- resolve(true)
- return
- }
-
- // try mirror install
- const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
- let mirrorInstallSuccess = false
-
- if (timezone === 'Asia/Shanghai') {
- mirrorInstallSuccess = await runInstall(proxyArgs)
- } else {
- mirrorInstallSuccess = await runInstall([])
- }
-
-
- fs.existsSync(installingLockPath) && fs.unlinkSync(installingLockPath)
-
- if (mirrorInstallSuccess) {
- fs.writeFileSync(installedLockPath, '')
- log.info('Mirror script completed successfully')
- console.log('end install dependencies (mirror)')
- spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath })
- resolve(true)
- } else {
- log.error('Both default and mirror install failed')
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send('install-dependencies-complete', { success: false, error: 'Both default and mirror install failed' });
- }
- resolve(false)
- }
- })
-}
-
-
export async function startBackend(setPort?: (port: number) => void): Promise {
console.log('start fastapi')
const uv_path = await getBinaryPath('uv')
const backendPath = getBackendPath()
const userData = app.getPath('userData');
+ const currentVersion = app.getVersion();
+ const venvPath = getVenvPath(currentVersion);
console.log('userData', userData)
+ console.log('Using venv path:', venvPath)
// Try to find an available port, with aggressive cleanup if needed
let port: number;
const portFile = path.join(userData, 'port.txt');
@@ -310,16 +153,26 @@ export async function startBackend(setPort?: (port: number) => void): Promise {
+ const displayFilteredLogs = (data: String) => {
if (!data) return;
const msg = data.toString().trimEnd();
+ //Detect if uv sync is run
+ detectInstallationLogs(msg);
+
if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) {
log.error(`BACKEND: ${msg}`);
} else if (msg.toLowerCase().includes("warn")) {
@@ -333,6 +186,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise {
+ //Implicitly runs uv sync
const node_process = spawn(
uv_path,
["run", "uvicorn", "main:api", "--port", port.toString(), "--loop", "asyncio"],
diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts
new file mode 100644
index 000000000..f4b8cd2fe
--- /dev/null
+++ b/electron/main/install-deps.ts
@@ -0,0 +1,672 @@
+import { app, BrowserWindow } from 'electron'
+import path from 'node:path'
+import log from 'electron-log'
+import { getMainWindow } from './init'
+import fs from 'node:fs'
+import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, cleanupOldVenvs, isBinaryExists, runInstallScript } from './utils/process'
+import { spawn } from 'child_process'
+import { safeMainWindowSend } from './utils/safeWebContentsSend'
+
+const userData = app.getPath('userData');
+const versionFile = path.join(userData, 'version.txt');
+
+export type PromiseReturnType = {
+ message: string;
+ success: boolean;
+}
+
+interface checkInstallProps {
+ win:BrowserWindow|null;
+ forceInstall?:boolean
+}
+// Read last run version and install dependencies on update
+export const checkAndInstallDepsOnUpdate = async ({win, forceInstall=false}:checkInstallProps):
+Promise => {
+ const currentVersion = app.getVersion();
+ let savedVersion = '';
+ const checkInstallOperations = {
+ getSavedVersion: ():boolean => {
+ // Check if version file exists
+ const versionExists = fs.existsSync(versionFile);
+ if (versionExists) {
+ log.info('[DEPS INSTALL] start check version', { currentVersion });
+ savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
+ log.info('[DEPS INSTALL] read saved version', { savedVersion });
+ } else {
+ log.info('[DEPS INSTALL] version file not exist, will create new file');
+ }
+ return versionExists;
+ },
+ handleUpdateNotification: (versionExists:boolean) => {
+ if (win && !win.isDestroyed()) {
+ win.webContents.send('update-notification', {
+ type: 'version-update',
+ currentVersion,
+ previousVersion: versionExists ? savedVersion : 'none',
+ reason: !versionExists ? 'version file not exist' : 'version not match'
+ });
+ } else {
+ log.warn('[DEPS INSTALL] Cannot send update notification - window not available');
+ }
+ },
+ createVersionFile: () => {
+ fs.writeFileSync(versionFile, currentVersion);
+ log.info('[DEPS INSTALL] version file updated', { currentVersion });
+ }
+ }
+
+ return new Promise(async (resolve, reject) => {
+ try {
+ const versionExists:boolean = checkInstallOperations.getSavedVersion();
+
+ // Check if command tools are installed
+ const uvExists = await isBinaryExists('uv');
+ const bunExists = await isBinaryExists('bun');
+ const toolsMissing = !uvExists || !bunExists;
+
+ // If version file does not exist or version does not match, reinstall dependencies
+ // Or if command tools are missing, need to install them
+ if (forceInstall || !versionExists || savedVersion !== currentVersion || toolsMissing) {
+ if (toolsMissing) {
+ log.info('[DEPS INSTALL] Command tools missing, starting installation...', {
+ uvExists,
+ bunExists
+ });
+ } else {
+ log.info('[DEPS INSTALL] version changed, prepare to reinstall uv dependencies...', {
+ currentVersion,
+ savedVersion: versionExists ? savedVersion : 'none',
+ reason: !versionExists ? 'version file not exist' : 'version not match'
+ });
+ }
+
+ // Notify frontend to update
+ checkInstallOperations.handleUpdateNotification(versionExists);
+
+ // Install dependencies (version.txt will be updated AFTER successful install)
+ const result = await installDependencies(currentVersion);
+ if (!result.success) {
+ log.error(' install dependencies failed');
+ resolve({ message: `Install dependencies failed, msg ${result.message}`, success: false });
+ return
+ }
+
+ // Update version file ONLY after successful installation
+ checkInstallOperations.createVersionFile();
+
+ resolve({ message: "Dependencies installed successfully after update", success: true });
+ log.info('[DEPS INSTALL] install dependencies complete');
+ return
+ } else {
+ log.info('[DEPS INSTALL] version not changed and tools installed, skip install dependencies', { currentVersion });
+ resolve({ message: "Version not changed and tools installed, skipped installation", success: true });
+ return
+ }
+ } catch (error) {
+ log.error(' check version and install dependencies error:', error);
+ resolve({ message: `Error checking version: ${error}`, success: false });
+ return
+ }
+ })
+}
+
+/**
+ * Check if command line tools are installed, install if not
+ */
+export async function installCommandTool(): Promise {
+ try {
+ const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise => {
+ if (await isBinaryExists(toolName)) {
+ return { message: `${toolName} already installed`, success: true };
+ }
+
+ console.log(`start install ${toolName}`);
+ await runInstallScript(scriptName);
+ const installed = await isBinaryExists(toolName);
+
+ if (installed) {
+ safeMainWindowSend('install-dependencies-log', {
+ type: 'stdout',
+ data: `${toolName} installed successfully`,
+ });
+ } else {
+ safeMainWindowSend('install-dependencies-complete', {
+ success: false,
+ code: 2,
+ error: `${toolName} installation failed (script exit code 2)`,
+ });
+ }
+
+ return {
+ message: installed ? `${toolName} installed successfully` : `${toolName} installation failed`,
+ success: installed
+ };
+ };
+
+ const uvResult = await ensureInstalled('uv', 'install-uv.js');
+ if (!uvResult.success) {
+ return { message: uvResult.message, success: false };
+ }
+
+ const bunResult = await ensureInstalled('bun', 'install-bun.js');
+ if (!bunResult.success) {
+ return { message: bunResult.message, success: false };
+ }
+
+ return { message: "Command tools installed successfully", success: true };
+ } catch (error) {
+ return { message: `Command tool installation failed: ${error}`, success: false };
+ }
+}
+
+let uv_path:string;
+const mainWindow = getMainWindow();
+const backendPath = getBackendPath();
+
+// Ensure backend directory exists
+if (!fs.existsSync(backendPath)) {
+ log.info(`Creating backend directory: ${backendPath}`);
+ fs.mkdirSync(backendPath, { recursive: true });
+}
+
+const installingLockPath = path.join(backendPath, 'uv_installing.lock')
+const installedLockPath = path.join(backendPath, 'uv_installed.lock')
+// const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple']
+const proxyArgs = ['--default-index', 'https://mirrors.aliyun.com/pypi/simple/']
+
+/**
+ * Get current installation status by checking lock files
+ * @returns Object with installation status information
+ */
+export async function getInstallationStatus(): Promise<{
+ isInstalling: boolean;
+ hasLockFile: boolean;
+ installedExists: boolean;
+}> {
+ try {
+ const installingExists = fs.existsSync(installingLockPath);
+ const installedExists = fs.existsSync(installedLockPath);
+
+ // If installing lock exists, installation is in progress
+ // If installed lock exists, installation completed previously
+ return {
+ isInstalling: installingExists,
+ hasLockFile: installingExists || installedExists,
+ installedExists: installedExists
+ };
+ } catch (error) {
+ console.error('[getInstallationStatus] Error checking installation status:', error);
+ return {
+ isInstalling: false,
+ hasLockFile: false,
+ installedExists: false
+ };
+ }
+}
+
+class InstallLogs {
+ private node_process;
+ private version: string;
+
+ constructor(extraArgs:string[], version: string) {
+ console.log('start install dependencies', extraArgs, 'version:', version)
+ const venvPath = getVenvPath(version);
+ this.version = version;
+
+ this.node_process = spawn(uv_path, [
+ 'sync',
+ '--no-dev',
+ '--cache-dir', getCachePath('uv_cache'),
+ ...extraArgs], {
+ cwd: backendPath,
+ env: {
+ ...process.env,
+ UV_TOOL_DIR: getCachePath('uv_tool'),
+ UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
+ UV_PROJECT_ENVIRONMENT: venvPath,
+ }
+ })
+ }
+
+ /**Display filtered logs based on severity */
+ displayFilteredLogs(data:String) {
+ if (!data) return;
+ const msg = data.toString().trimEnd();
+ //Detect if uv sync is run
+ detectInstallationLogs(msg);
+ if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) {
+ log.error(`BACKEND: [DEPS INSTALL] ${msg}`);
+ safeMainWindowSend('install-dependencies-log', { type: 'stderr', data: data.toString() });
+ } else {
+ log.info(`BACKEND: [DEPS INSTALL] ${msg}`);
+ safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() });
+ }
+ }
+
+ /**Handle stdout data */
+ onStdout() {
+ this.node_process.stdout.on('data', (data:any) => {
+ this.displayFilteredLogs(data);
+ })
+ }
+
+ /**Handle stderr data */
+ onStderr() {
+ this.node_process.stderr.on('data', (data:any) => {
+ this.displayFilteredLogs(data);
+ })
+ }
+
+ /**Handle process close event */
+ onClose(resolveInner:(code: number | null) => void) {
+ this.node_process.on('close', resolveInner);
+ }
+
+ /**
+ * Set installing Lock Path
+ * Creates uv_installing.lock file to indicate installation in progress
+ * Creates backend directory if not exists
+ */
+ static setLockPath() {
+ if (!fs.existsSync(backendPath)) {
+ fs.mkdirSync(backendPath, { recursive: true })
+ }
+ fs.writeFileSync(installingLockPath, '')
+ }
+
+ /**Clean installing Lock Path */
+ static cleanLockPath() {
+ if (fs.existsSync(installingLockPath)) {
+ fs.unlinkSync(installingLockPath);
+ }
+ }
+}
+
+const runInstall = (extraArgs: string[], version: string) => {
+ const installLogs = new InstallLogs(extraArgs, version);
+ return new Promise((resolveInner, rejectInner) => {
+ try {
+ installLogs.onStdout();
+ installLogs.onStderr();
+ installLogs.onClose((code) => {
+ console.log('install dependencies end', code === 0)
+ InstallLogs.cleanLockPath()
+ resolveInner({
+ message: code === 0 ? "Installation completed successfully" : `Installation failed with code ${code}`,
+ success: code === 0
+ })
+ })
+ } catch (err) {
+ log.error('run install failed', err)
+ // Clean up uv_installing.lock file if installation fails
+ InstallLogs.cleanLockPath();
+ rejectInner({ message: `Installation failed: ${err}`, success: false })
+ }
+ })
+}
+
+export async function installDependencies(version: string): Promise {
+ uv_path = await getBinaryPath('uv');
+ const venvPath = getVenvPath(version);
+
+ const handleInstallOperations = {
+ spawnBabel: (message:"mirror"|"main"="main") => {
+ fs.writeFileSync(installedLockPath, '')
+ log.info('[DEPS INSTALL] Script completed successfully')
+ console.log(`Install Dependencies completed ${message} for version ${version}`)
+ console.log(`Virtual environment path: ${venvPath}`)
+ spawn(uv_path, ['run', 'task', 'babel'], {
+ cwd: backendPath,
+ env: {
+ ...process.env,
+ UV_PROJECT_ENVIRONMENT: venvPath,
+ }
+ })
+ },
+ notifyInstallDependenciesPage: ():boolean => {
+ const success = safeMainWindowSend('install-dependencies-start');
+ if (!success) {
+ log.warn('[DEPS INSTALL] Main window not available, continuing installation without UI updates');
+ }
+ return success;
+ },
+ installHybridBrowserDependencies: async (): Promise => {
+ try {
+ // Find the hybrid_browser_toolkit ts directory in the virtual environment
+ // Need to determine the Python version to construct the correct path
+ let sitePackagesPath: string | null = null;
+ const libPath = path.join(venvPath, 'lib');
+
+ // Try to find the site-packages directory (it varies by Python version)
+ if (fs.existsSync(libPath)) {
+ const libContents = fs.readdirSync(libPath);
+ const pythonDir = libContents.find(name => name.startsWith('python'));
+ if (pythonDir) {
+ sitePackagesPath = path.join(libPath, pythonDir, 'site-packages');
+ }
+ }
+
+ if (!sitePackagesPath || !fs.existsSync(sitePackagesPath)) {
+ log.warn('[DEPS INSTALL] site-packages directory not found in venv, skipping npm install');
+ return true; // Not an error if the venv structure is different
+ }
+
+ const toolkitPath = path.join(sitePackagesPath, 'camel', 'toolkits', 'hybrid_browser_toolkit', 'ts');
+
+ if (!fs.existsSync(toolkitPath)) {
+ log.warn('[DEPS INSTALL] hybrid_browser_toolkit ts directory not found at ' + toolkitPath + ', skipping npm install');
+ return true; // Not an error if the toolkit isn't installed
+ }
+
+ log.info('[DEPS INSTALL] Installing hybrid_browser_toolkit npm dependencies...');
+ safeMainWindowSend('install-dependencies-log', {
+ type: 'stdout',
+ data: 'Installing browser toolkit dependencies...\n'
+ });
+
+ // Try to find npm - first try system npm, then try uv run npm
+ let npmCommand: string[];
+ const testNpm = spawn('npm', ['--version'], { shell: true });
+ const npmExists = await new Promise(resolve => {
+ testNpm.on('close', (code) => resolve(code === 0));
+ testNpm.on('error', () => resolve(false));
+ });
+
+ if (npmExists) {
+ // Use system npm directly
+ npmCommand = ['npm'];
+ log.info('[DEPS INSTALL] Using system npm for installation');
+ } else {
+ // Try uv run npm (might not work if nodejs-wheel isn't properly set up)
+ npmCommand = [uv_path, 'run', 'npm'];
+ log.info('[DEPS INSTALL] Attempting to use uv run npm');
+ }
+
+ // Run npm install
+ const npmCacheDir = path.join(venvPath, '.npm-cache');
+ if (!fs.existsSync(npmCacheDir)) {
+ fs.mkdirSync(npmCacheDir, { recursive: true });
+ }
+
+ const npmInstall = spawn(npmCommand[0], [...npmCommand.slice(1), 'install'], {
+ cwd: toolkitPath,
+ env: {
+ ...process.env,
+ UV_PROJECT_ENVIRONMENT: venvPath,
+ npm_config_cache: npmCacheDir,
+ },
+ shell: true // Important for Windows
+ });
+
+ await new Promise((resolve, reject) => {
+ if (npmInstall.stdout) {
+ npmInstall.stdout.on('data', (data) => {
+ log.info(`[DEPS INSTALL] npm install: ${data}`);
+ safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() });
+ });
+ }
+
+ if (npmInstall.stderr) {
+ npmInstall.stderr.on('data', (data) => {
+ log.warn(`[DEPS INSTALL] npm install stderr: ${data}`);
+ safeMainWindowSend('install-dependencies-log', { type: 'stderr', data: data.toString() });
+ });
+ }
+
+ npmInstall.on('close', (code) => {
+ if (code === 0) {
+ log.info('[DEPS INSTALL] npm install completed successfully');
+ resolve();
+ } else {
+ log.error(`[DEPS INSTALL] npm install failed with code ${code}`);
+ reject(new Error(`npm install failed with code ${code}`));
+ }
+ });
+
+ npmInstall.on('error', (err) => {
+ log.error(`[DEPS INSTALL] npm install process error: ${err}`);
+ reject(err);
+ });
+ });
+
+ // Run npm build (use the same npm command as install)
+ log.info('[DEPS INSTALL] Building hybrid_browser_toolkit TypeScript...');
+ safeMainWindowSend('install-dependencies-log', {
+ type: 'stdout',
+ data: 'Building browser toolkit TypeScript...\n'
+ });
+
+ const buildArgs = npmCommand[0] === 'npm' ? ['run', 'build'] : [...npmCommand.slice(1), 'run', 'build'];
+ const npmBuild = spawn(npmCommand[0], buildArgs, {
+ cwd: toolkitPath,
+ env: {
+ ...process.env,
+ UV_PROJECT_ENVIRONMENT: venvPath,
+ npm_config_cache: npmCacheDir,
+ },
+ shell: true // Important for Windows
+ });
+
+ await new Promise((resolve, reject) => {
+ if (npmBuild.stdout) {
+ npmBuild.stdout.on('data', (data) => {
+ log.info(`[DEPS INSTALL] npm build: ${data}`);
+ safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() });
+ });
+ }
+
+ if (npmBuild.stderr) {
+ npmBuild.stderr.on('data', (data) => {
+ // TypeScript build warnings are common, don't treat as errors
+ log.info(`[DEPS INSTALL] npm build output: ${data}`);
+ safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() });
+ });
+ }
+
+ npmBuild.on('close', (code) => {
+ if (code === 0) {
+ log.info('[DEPS INSTALL] TypeScript build completed successfully');
+ resolve();
+ } else {
+ log.error(`[DEPS INSTALL] TypeScript build failed with code ${code}`);
+ reject(new Error(`TypeScript build failed with code ${code}`));
+ }
+ });
+
+ npmBuild.on('error', (err) => {
+ log.error(`[DEPS INSTALL] npm build process error: ${err}`);
+ reject(err);
+ });
+ });
+
+ // Optionally install Playwright browsers
+ try {
+ log.info('[DEPS INSTALL] Installing Playwright browsers...');
+ const npxCommand = npmCommand[0] === 'npm' ? ['npx'] : [uv_path, 'run', 'npx'];
+ const playwrightInstall = spawn(npxCommand[0], [...npxCommand.slice(1), 'playwright', 'install'], {
+ cwd: toolkitPath,
+ env: {
+ ...process.env,
+ UV_PROJECT_ENVIRONMENT: venvPath,
+ },
+ shell: true
+ });
+
+ await new Promise((resolve) => {
+ playwrightInstall.on('close', (code) => {
+ if (code === 0) {
+ log.info('[DEPS INSTALL] Playwright browsers installed successfully');
+ // Create marker file
+ const markerPath = path.join(toolkitPath, '.playwright_installed');
+ fs.writeFileSync(markerPath, 'installed');
+ } else {
+ log.warn('[DEPS INSTALL] Playwright installation failed, but continuing anyway');
+ }
+ resolve();
+ });
+
+ playwrightInstall.on('error', (err) => {
+ log.warn('[DEPS INSTALL] Playwright installation process error:', err);
+ resolve(); // Non-critical, continue
+ });
+ });
+ } catch (error) {
+ log.warn('[DEPS INSTALL] Failed to install Playwright browsers:', error);
+ // Non-critical, continue
+ }
+
+ log.info('[DEPS INSTALL] hybrid_browser_toolkit dependencies installed successfully');
+ return true;
+ } catch (error) {
+ log.error('[DEPS INSTALL] Failed to install hybrid_browser_toolkit dependencies:', error);
+ // Don't fail the entire installation if this fails
+ return false;
+ }
+ }
+ }
+
+ return new Promise(async (resolve, reject) => {
+ console.log('start install dependencies')
+ const mainWindowAvailable = handleInstallOperations.notifyInstallDependenciesPage();
+
+ if (!mainWindowAvailable) {
+ log.info('[DEPS INSTALL] Proceeding with installation without UI notifications');
+ }
+
+ const isInstalCommandTool = await installCommandTool()
+ if (!isInstalCommandTool.success) {
+ resolve({ message: "Command tool installation failed", success: false })
+ return
+ }
+
+ // Set Installing Lock Files
+ InstallLogs.setLockPath();
+
+ // try default install
+ const installSuccess = await runInstall([], version)
+ if (installSuccess.success) {
+ // Install hybrid_browser_toolkit npm dependencies after Python packages are installed
+ log.info('[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...')
+ await handleInstallOperations.installHybridBrowserDependencies()
+
+ handleInstallOperations.spawnBabel()
+
+ // Clean up old venvs after successful installation
+ log.info('[DEPS INSTALL] Cleaning up old virtual environments...')
+ await cleanupOldVenvs(version)
+ log.info('[DEPS INSTALL] Old venvs cleanup completed')
+
+ resolve({ message: "Dependencies installed successfully", success: true })
+ return
+ }
+
+ // try mirror install
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
+ let mirrorInstallSuccess: PromiseReturnType = { message: "", success: false }
+ mirrorInstallSuccess = (timezone === 'Asia/Shanghai')? await runInstall(proxyArgs, version) :await runInstall([], version)
+
+ if (mirrorInstallSuccess.success) {
+ // Install hybrid_browser_toolkit npm dependencies after Python packages are installed
+ log.info('[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...')
+ await handleInstallOperations.installHybridBrowserDependencies()
+
+ handleInstallOperations.spawnBabel("mirror")
+
+ // Clean up old venvs after successful installation
+ log.info('[DEPS INSTALL] Cleaning up old virtual environments...')
+ await cleanupOldVenvs(version)
+ log.info('[DEPS INSTALL] Old venvs cleanup completed')
+
+ resolve({ message: "Dependencies installed successfully with mirror", success: true })
+ } else {
+ log.error('Both default and mirror install failed')
+ safeMainWindowSend('install-dependencies-complete', {
+ success: false,
+ error: 'Both default and mirror install failed'
+ });
+ resolve({ message: "Both default and mirror install failed", success: false })
+ }
+ })
+}
+
+let dependencyInstallationDetected = false;
+let installationNotificationSent = false;
+export function detectInstallationLogs(msg:string) {
+ // Check for UV dependency installation patterns
+ const installPatterns = [
+ "Resolved", // UV resolving dependencies
+ "Downloaded", // UV downloading packages
+ "Installing", // UV installing packages
+ "Built", // UV building packages
+ "Prepared", // UV preparing virtual environment
+ "Syncing", // UV sync process
+ "Creating virtualenv", // Virtual environment creation
+ "Updating", // UV updating packages
+ "× No solution found when resolving dependencies", // Dependency resolution issues
+ "Audited" // UV auditing dependencies
+ ];
+
+ // Detect if UV is installing dependencies
+ if (!dependencyInstallationDetected && installPatterns.some(pattern =>
+ msg.includes(pattern) && !msg.includes("Uvicorn running on")
+ )) {
+ dependencyInstallationDetected = true;
+ log.info('[BACKEND STARTUP] UV dependency installation detected during uvicorn startup');
+
+ // Create installing lock file to maintain consistency with install-deps.ts
+ InstallLogs.setLockPath();
+ log.info('[BACKEND STARTUP] Created uv_installing.lock file');
+
+ // Notify frontend that installation has started (only once)
+ if (!installationNotificationSent) {
+ installationNotificationSent = true;
+ const notificationSent = safeMainWindowSend('install-dependencies-start');
+ if (notificationSent) {
+ log.info('[BACKEND STARTUP] Notified frontend of dependency installation start');
+ } else {
+ log.warn('[BACKEND STARTUP] Failed to notify frontend of dependency installation start');
+ }
+ }
+ }
+
+ // Send installation logs to frontend if installation was detected
+ if (dependencyInstallationDetected && !msg.includes("Uvicorn running on")) {
+ safeMainWindowSend('install-dependencies-log', {
+ type: msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback") ? 'stderr' : 'stdout',
+ data: msg
+ });
+ }
+
+ // Check if installation is complete (uvicorn starts successfully)
+ if (dependencyInstallationDetected && msg.includes("Uvicorn running on")) {
+ log.info('[BACKEND STARTUP] UV dependency installation completed, uvicorn started successfully');
+
+ // Clean up installing lock and create installed lock
+ InstallLogs.cleanLockPath();
+ fs.writeFileSync(installedLockPath, '');
+ log.info('[BACKEND STARTUP] Created uv_installed.lock file');
+
+ safeMainWindowSend('install-dependencies-complete', {
+ success: true,
+ message: 'Dependencies installed successfully during backend startup'
+ });
+ }
+
+ // Handle installation failures
+ if (dependencyInstallationDetected && (
+ msg.toLowerCase().includes("failed to resolve dependencies") ||
+ msg.toLowerCase().includes("installation failed") ||
+ msg.includes("× No solution found when resolving dependencies")
+ )) {
+ log.error('[BACKEND STARTUP] UV dependency installation failed');
+
+ // Clean up installing lock file
+ InstallLogs.cleanLockPath();
+ log.info('[BACKEND STARTUP] Cleaned up uv_installing.lock file after failure');
+
+ safeMainWindowSend('install-dependencies-complete', {
+ success: false,
+ error: 'Dependency installation failed during backend startup'
+ });
+ }
+}
\ No newline at end of file
diff --git a/electron/main/utils/envUtil.ts b/electron/main/utils/envUtil.ts
index 850c40cbc..7d0a1276c 100644
--- a/electron/main/utils/envUtil.ts
+++ b/electron/main/utils/envUtil.ts
@@ -7,7 +7,14 @@ export const ENV_END = '# === MCP INTEGRATION ENV END ===';
export function getEnvPath(email: string) {
const tempEmail = email.split("@")[0].replace(/[\\/*?:"<>|\s]/g, "_").replace(".", "_")
- const envPath = path.join(os.homedir(), '.eigent', '.env.' + tempEmail)
+ const eigentDir = path.join(os.homedir(), '.eigent')
+
+ // Ensure .eigent directory exists
+ if (!fs.existsSync(eigentDir)) {
+ fs.mkdirSync(eigentDir, { recursive: true });
+ }
+
+ const envPath = path.join(eigentDir, '.env.' + tempEmail)
const defaultEnv = path.join(process.resourcesPath, 'backend', '.env');
if (!fs.existsSync(envPath) && fs.existsSync(defaultEnv)) {
fs.copyFileSync(defaultEnv, envPath);
diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts
index 5d1bcd437..232860a8e 100644
--- a/electron/main/utils/process.ts
+++ b/electron/main/utils/process.ts
@@ -57,24 +57,81 @@ export async function getBinaryName(name: string): Promise {
}
export async function getBinaryPath(name?: string): Promise {
- if (!name) {
- return path.join(os.homedir(), '.eigent', 'bin')
- }
- const binaryName = await getBinaryName(name)
const binariesDir = path.join(os.homedir(), '.eigent', 'bin')
- const binariesDirExists = await fs.existsSync(binariesDir)
- return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName
+ // Ensure .eigent/bin directory exists
+ if (!fs.existsSync(binariesDir)) {
+ fs.mkdirSync(binariesDir, { recursive: true })
+ }
+
+ if (!name) {
+ return binariesDir
+ }
+
+ const binaryName = await getBinaryName(name)
+ return path.join(binariesDir, binaryName)
}
export function getCachePath(folder: string): string {
const cacheDir = path.join(os.homedir(), '.eigent', 'cache', folder)
- console.log('cacheDir+++++++++++++++++++++++++++')
- console.log('Cache directory:', cacheDir)
- console.log('cacheDir--------------------')
+
+ // Ensure cache directory exists
+ if (!fs.existsSync(cacheDir)) {
+ fs.mkdirSync(cacheDir, { recursive: true })
+ }
+
return cacheDir
}
+export function getVenvPath(version: string): string {
+ const venvDir = path.join(os.homedir(), '.eigent', 'venvs', `backend-${version}`)
+
+ // Ensure venvs directory exists (parent of the actual venv)
+ const venvsBaseDir = path.dirname(venvDir)
+ if (!fs.existsSync(venvsBaseDir)) {
+ fs.mkdirSync(venvsBaseDir, { recursive: true })
+ }
+
+ return venvDir
+}
+
+export function getVenvsBaseDir(): string {
+ return path.join(os.homedir(), '.eigent', 'venvs')
+}
+
+export async function cleanupOldVenvs(currentVersion: string): Promise {
+ const venvsBaseDir = getVenvsBaseDir()
+
+ // Check if venvs directory exists
+ if (!fs.existsSync(venvsBaseDir)) {
+ return
+ }
+
+ try {
+ const entries = fs.readdirSync(venvsBaseDir, { withFileTypes: true })
+
+ for (const entry of entries) {
+ if (entry.isDirectory() && entry.name.startsWith('backend-')) {
+ const versionMatch = entry.name.match(/^backend-(.+)$/)
+ if (versionMatch && versionMatch[1] !== currentVersion) {
+ const oldVenvPath = path.join(venvsBaseDir, entry.name)
+ console.log(`Cleaning up old venv: ${oldVenvPath}`)
+
+ try {
+ // Remove old venv directory recursively
+ fs.rmSync(oldVenvPath, { recursive: true, force: true })
+ console.log(`Successfully removed old venv: ${entry.name}`)
+ } catch (err) {
+ console.error(`Failed to remove old venv ${entry.name}:`, err)
+ }
+ }
+ }
+ }
+ } catch (err) {
+ console.error('Error during venv cleanup:', err)
+ }
+}
+
export async function isBinaryExists(name: string): Promise {
const cmd = await getBinaryPath(name)
diff --git a/electron/main/utils/safeWebContentsSend.ts b/electron/main/utils/safeWebContentsSend.ts
new file mode 100644
index 000000000..0e72da65d
--- /dev/null
+++ b/electron/main/utils/safeWebContentsSend.ts
@@ -0,0 +1,20 @@
+import log from 'electron-log'
+import { getMainWindow } from "../init";
+
+/**
+ * Safely send message to main window if it exists and is not destroyed
+ * @param channel - The IPC channel to send message to
+ * @param data - The data to send
+ */
+function safeMainWindowSend(channel: string, data?: any) {
+ const mainWindow = getMainWindow();
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ mainWindow.webContents.send(channel, data);
+ return true;
+ } else {
+ log.warn(`[WEBCONTENTS SEND] Cannot send message to main window: ${channel}`, data);
+ return false;
+ }
+}
+
+export {safeMainWindowSend}
\ No newline at end of file
diff --git a/electron/preload/index.ts b/electron/preload/index.ts
index 0c89d17d2..d1c5e9e85 100644
--- a/electron/preload/index.ts
+++ b/electron/preload/index.ts
@@ -62,9 +62,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
deleteFolder: (email: string) => ipcRenderer.invoke('delete-folder', email),
getMcpConfigPath: (email: string) => ipcRenderer.invoke('get-mcp-config-path', email),
// install dependencies related API
- installDependencies: () => ipcRenderer.invoke('install-dependencies'),
- frontendReady: () => ipcRenderer.invoke('frontend-ready'),
+ checkAndInstallDepsOnUpdate: () => ipcRenderer.invoke('install-dependencies'),
checkInstallBrowser: () => ipcRenderer.invoke('check-install-browser'),
+ getInstallationStatus: () => ipcRenderer.invoke('get-installation-status'),
onInstallDependenciesStart: (callback: () => void) => {
ipcRenderer.on('install-dependencies-start', callback);
},
diff --git a/package.json b/package.json
index 1fed1dac2..42fafd1ff 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "eigent",
- "version": "0.0.66",
+ "version": "0.0.72",
"main": "dist-electron/main/index.js",
"description": "Eigent",
"author": "Eigent.AI",
@@ -59,6 +59,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"csv-parser": "^3.2.0",
+ "dompurify": "^3.2.7",
"electron-log": "^5.4.0",
"electron-updater": "^6.3.9",
"embla-carousel-autoplay": "^8.6.0",
@@ -92,6 +93,7 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/archiver": "^6.0.3",
+ "@types/dompurify": "^3.0.5",
"@types/lodash-es": "^4.17.12",
"@types/papaparse": "^5.3.16",
"@types/react": "^18.3.12",
diff --git a/server/alembic/versions/2025_09_28_1621-d74ab2a44600_drop_mcp_user_foreign_key_constraint.py b/server/alembic/versions/2025_09_28_1621-d74ab2a44600_drop_mcp_user_foreign_key_constraint.py
new file mode 100644
index 000000000..9bd085337
--- /dev/null
+++ b/server/alembic/versions/2025_09_28_1621-d74ab2a44600_drop_mcp_user_foreign_key_constraint.py
@@ -0,0 +1,84 @@
+"""drop_mcp_user_foreign_key_constraint
+
+Revision ID: d74ab2a44600
+Revises: 0001_init
+Create Date: 2025-09-28 16:21:06.930093
+
+"""
+
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+import sqlmodel.sql.sqltypes
+
+
+# revision identifiers, used by Alembic.
+revision: str = "d74ab2a44600"
+down_revision: Union[str, None] = "0001_init"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ """Upgrade schema."""
+ # Drop the foreign key constraint for mcp_id in mcp_user table
+ # Use try-catch to handle cases where constraint might not exist or have different name
+
+ try:
+ op.drop_constraint("mcp_user_mcp_id_fkey", "mcp_user", type_="foreignkey")
+ except Exception as e:
+ # If the expected constraint name doesn't work, try to find it dynamically
+ try:
+ connection = op.get_bind()
+ inspector = sa.inspect(connection)
+ fk_constraints = inspector.get_foreign_keys("mcp_user")
+
+ # Find the constraint that references mcp_id -> mcp.id
+ target_constraint = None
+ for fk in fk_constraints:
+ if (fk.get("constrained_columns") == ["mcp_id"] and
+ fk.get("referred_table") == "mcp" and
+ fk.get("referred_columns") == ["id"]):
+ target_constraint = fk.get("name")
+ break
+
+ if target_constraint:
+ op.drop_constraint(target_constraint, "mcp_user", type_="foreignkey")
+ else:
+ print("Warning: No foreign key constraint found for mcp_user.mcp_id -> mcp.id")
+ except Exception as e2:
+ print(f"Warning: Could not drop foreign key constraint: {e2}")
+
+
+def downgrade() -> None:
+ """Downgrade schema."""
+ # Re-add the foreign key constraint for mcp_id in mcp_user table
+ # Check if the constraint already exists before creating it
+
+ try:
+ connection = op.get_bind()
+ inspector = sa.inspect(connection)
+ fk_constraints = inspector.get_foreign_keys("mcp_user")
+
+ # Check if the constraint already exists
+ constraint_exists = False
+ for fk in fk_constraints:
+ if (fk.get("constrained_columns") == ["mcp_id"] and
+ fk.get("referred_table") == "mcp" and
+ fk.get("referred_columns") == ["id"]):
+ constraint_exists = True
+ break
+
+ if not constraint_exists:
+ op.create_foreign_key(
+ "mcp_user_mcp_id_fkey",
+ "mcp_user",
+ "mcp",
+ ["mcp_id"],
+ ["id"]
+ )
+ else:
+ print("Info: Foreign key constraint for mcp_user.mcp_id -> mcp.id already exists")
+ except Exception as e:
+ print(f"Warning: Could not check or create foreign key constraint: {e}")
diff --git a/server/app/controller/mcp/mcp_controller.py b/server/app/controller/mcp/mcp_controller.py
index 1dd5ac5ef..1a38c7580 100644
--- a/server/app/controller/mcp/mcp_controller.py
+++ b/server/app/controller/mcp/mcp_controller.py
@@ -1,3 +1,4 @@
+import os
from typing import Dict
from fastapi import Depends, HTTPException, APIRouter
from fastapi_babel import _
@@ -11,6 +12,8 @@ from app.model.mcp.mcp import Mcp, McpOut, McpType
from app.model.mcp.mcp_env import McpEnv, Status as McpEnvStatus
from app.model.mcp.mcp_user import McpImportType, McpUser, Status
from loguru import logger
+from camel.toolkits.mcp_toolkit import MCPToolkit
+from app.component.environment import env
from app.component.validator.McpServer import (
McpRemoteServer,
@@ -22,6 +25,45 @@ from app.component.validator.McpServer import (
router = APIRouter(tags=["Mcp Servers"])
+async def pre_instantiate_mcp_toolkit(config_dict: dict) -> bool:
+ """
+ Pre-instantiate MCP toolkit to complete authentication process
+
+ Args:
+ config_dict: MCP server configuration dictionary
+
+ Returns:
+ bool: Whether successfully instantiated and connected
+ """
+ try:
+ # Ensure unified auth directory for all mcp servers
+ for server_config in config_dict.get("mcpServers", {}).values():
+ if "env" not in server_config:
+ server_config["env"] = {}
+ # Set global auth directory to persist authentication across tasks
+ if "MCP_REMOTE_CONFIG_DIR" not in server_config["env"]:
+ server_config["env"]["MCP_REMOTE_CONFIG_DIR"] = env(
+ "MCP_REMOTE_CONFIG_DIR",
+ os.path.expanduser("~/.mcp-auth")
+ )
+
+ # Create MCP toolkit and attempt to connect
+ mcp_toolkit = MCPToolkit(config_dict=config_dict, timeout=30)
+ await mcp_toolkit.connect()
+
+ # Get tools list to ensure connection is successful
+ tools = mcp_toolkit.get_tools()
+ logger.info(f"Successfully pre-instantiated MCP toolkit with {len(tools)} tools")
+
+ # Disconnect, authentication info is already saved
+ await mcp_toolkit.disconnect()
+ return True
+
+ except Exception as e:
+ logger.warning(f"Failed to pre-instantiate MCP toolkit: {e!r}")
+ return False
+
+
@router.get("/mcps", name="mcp list")
async def gets(
keyword: str | None = None,
@@ -71,7 +113,23 @@ async def install(mcp_id: int, session: Session = Depends(session), auth: Auth =
exists = session.exec(select(McpUser).where(McpUser.mcp_id == mcp.id, McpUser.user_id == auth.user.id)).first()
if exists:
raise HTTPException(status_code=400, detail=_("mcp is installed"))
+
install_command: dict = mcp.install_command
+
+ # Pre-instantiate MCP toolkit for authentication
+ config_dict = {
+ "mcpServers": {
+ mcp.key: install_command
+ }
+ }
+
+ try:
+ success = await pre_instantiate_mcp_toolkit(config_dict)
+ if not success:
+ logger.warning(f"Pre-instantiation failed for MCP {mcp.key}, but continuing with installation")
+ except Exception as e:
+ logger.warning(f"Exception during pre-instantiation for MCP {mcp.key}: {e}")
+
mcp_user = McpUser(
mcp_id=mcp.id,
user_id=auth.user.id,
@@ -100,7 +158,26 @@ async def import_mcp(
if not is_valid:
raise HTTPException(status_code=400, detail=res)
mcp_data: Dict[str, McpServerItem] = res.mcpServers
+
for name, data in mcp_data.items():
+ # Pre-instantiate MCP toolkit for authentication
+ config_dict = {
+ "mcpServers": {
+ name: {
+ "command": data.command,
+ "args": data.args,
+ "env": data.env or {}
+ }
+ }
+ }
+
+ try:
+ success = await pre_instantiate_mcp_toolkit(config_dict)
+ if not success:
+ logger.warning(f"Pre-instantiation failed for local MCP {name}, but continuing with installation")
+ except Exception as e:
+ logger.warning(f"Exception during pre-instantiation for local MCP {name}: {e}")
+
mcp_user = McpUser(
mcp_id=0,
user_id=auth.user.id,
@@ -114,12 +191,17 @@ async def import_mcp(
env=data.env,
server_url=None,
)
- break
+ mcp_user.save()
+ return {"message": "Local MCP servers imported successfully", "count": len(mcp_data)}
elif mcp_type == McpImportType.Remote:
is_valid, res = validate_mcp_remote_servers(mcp_data)
if not is_valid:
raise HTTPException(status_code=400, detail=res)
data: McpRemoteServer = res
+
+ # For remote servers, we don't need to pre-instantiate as they typically don't require authentication
+ # but we can still try to validate the connection if needed
+
mcp_user = McpUser(
mcp_id=0,
user_id=auth.user.id,
@@ -128,5 +210,5 @@ async def import_mcp(
mcp_name=data.server_name,
server_url=data.server_url,
)
- mcp_user.save()
- return mcp_user
+ mcp_user.save()
+ return mcp_user
diff --git a/server/app/controller/mcp/user_controller.py b/server/app/controller/mcp/user_controller.py
index 2beda01f7..12b979abb 100644
--- a/server/app/controller/mcp/user_controller.py
+++ b/server/app/controller/mcp/user_controller.py
@@ -1,3 +1,4 @@
+import os
from typing import List, Optional
from fastapi import Depends, HTTPException, Query, Response, APIRouter
from sqlmodel import Session, select
@@ -5,11 +6,53 @@ from app.component.database import session
from app.component.auth import Auth, auth_must
from fastapi_babel import _
from app.model.mcp.mcp_user import McpUser, McpUserIn, McpUserOut, McpUserUpdate, Status
+from app.model.mcp.mcp import Mcp
from loguru import logger
+from camel.toolkits.mcp_toolkit import MCPToolkit
+from app.component.environment import env
router = APIRouter(tags=["McpUser Management"])
+async def pre_instantiate_mcp_toolkit(config_dict: dict) -> bool:
+ """
+ Pre-instantiate MCP toolkit to complete authentication process
+
+ Args:
+ config_dict: MCP server configuration dictionary
+
+ Returns:
+ bool: Whether successfully instantiated and connected
+ """
+ try:
+ # Ensure unified auth directory for all mcp servers
+ for server_config in config_dict.get("mcpServers", {}).values():
+ if "env" not in server_config:
+ server_config["env"] = {}
+ # Set global auth directory to persist authentication across tasks
+ if "MCP_REMOTE_CONFIG_DIR" not in server_config["env"]:
+ server_config["env"]["MCP_REMOTE_CONFIG_DIR"] = env(
+ "MCP_REMOTE_CONFIG_DIR",
+ os.path.expanduser("~/.mcp-auth")
+ )
+
+ # Create MCP toolkit and attempt to connect
+ mcp_toolkit = MCPToolkit(config_dict=config_dict, timeout=30)
+ await mcp_toolkit.connect()
+
+ # Get tools list to ensure connection is successful
+ tools = mcp_toolkit.get_tools()
+ logger.info(f"Successfully pre-instantiated MCP toolkit with {len(tools)} tools")
+
+ # Disconnect, authentication info is already saved
+ await mcp_toolkit.disconnect()
+ return True
+
+ except Exception as e:
+ logger.warning(f"Failed to pre-instantiate MCP toolkit: {e!r}")
+ return False
+
+
@router.get("/mcp/users", name="list mcp users", response_model=List[McpUserOut])
async def list_mcp_users(
mcp_id: Optional[int] = None,
@@ -42,6 +85,24 @@ async def create_mcp_user(mcp_user: McpUserIn, session: Session = Depends(sessio
).first()
if exists:
raise HTTPException(status_code=400, detail=_("mcp is installed"))
+
+ # Get MCP configuration from the main Mcp table
+ mcp = session.get(Mcp, mcp_user.mcp_id)
+ if mcp and mcp.install_command:
+ # Pre-instantiate MCP toolkit for authentication
+ config_dict = {
+ "mcpServers": {
+ mcp.key: mcp.install_command
+ }
+ }
+
+ try:
+ success = await pre_instantiate_mcp_toolkit(config_dict)
+ if not success:
+ logger.warning(f"Pre-instantiation failed for MCP {mcp.key}, but continuing with user creation")
+ except Exception as e:
+ logger.warning(f"Exception during pre-instantiation for MCP {mcp.key}: {e}")
+
db_mcp_user = McpUser(mcp_id=mcp_user.mcp_id, user_id=auth.user.id, env=mcp_user.env)
session.add(db_mcp_user)
session.commit()
diff --git a/server/app/model/config/config.py b/server/app/model/config/config.py
index 72e213ee7..022a520d5 100644
--- a/server/app/model/config/config.py
+++ b/server/app/model/config/config.py
@@ -45,7 +45,7 @@ class ConfigInfo:
},
ConfigGroup.NOTION.value: {
"env_vars": ["MCP_REMOTE_CONFIG_DIR"],
- "toolkit": "notion_toolkit",
+ "toolkit": "notion_mcp_toolkit",
},
ConfigGroup.TWITTER.value: {
"env_vars": [
diff --git a/server/app/model/mcp/mcp_user.py b/server/app/model/mcp/mcp_user.py
index 04e202a92..779cd72f9 100644
--- a/server/app/model/mcp/mcp_user.py
+++ b/server/app/model/mcp/mcp_user.py
@@ -71,7 +71,6 @@ class McpUserUpdate(BaseModel):
server_url: Optional[str] = None
command: Optional[str] = None
args: Optional[str] = None
- env: Optional[dict] = None
mcp_key: Optional[str] = None
diff --git a/server/docker-compose.yml b/server/docker-compose.yml
index 124d3d93f..193ee5713 100644
--- a/server/docker-compose.yml
+++ b/server/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3.8'
-
services:
# PostgreSQL Database
postgres:
diff --git a/src/assets/wechat_qr_1.jpg b/src/assets/wechat_qr_1.jpg
index de106ba0c..a3814f7f0 100644
Binary files a/src/assets/wechat_qr_1.jpg and b/src/assets/wechat_qr_1.jpg differ
diff --git a/src/assets/wechat_qr_2.jpg b/src/assets/wechat_qr_2.jpg
index 186da1a82..c560e36c9 100644
Binary files a/src/assets/wechat_qr_2.jpg and b/src/assets/wechat_qr_2.jpg differ
diff --git a/src/assets/wechat_qr_3.jpg b/src/assets/wechat_qr_3.jpg
index 9b852f62d..d5d31392f 100644
Binary files a/src/assets/wechat_qr_3.jpg and b/src/assets/wechat_qr_3.jpg differ
diff --git a/src/assets/wechat_qr_4.jpg b/src/assets/wechat_qr_4.jpg
index 9efee4d64..7ac6837ff 100644
Binary files a/src/assets/wechat_qr_4.jpg and b/src/assets/wechat_qr_4.jpg differ
diff --git a/src/components/AddWorker/IntegrationList.tsx b/src/components/AddWorker/IntegrationList.tsx
index 82043cb3b..8a2dc55d2 100644
--- a/src/components/AddWorker/IntegrationList.tsx
+++ b/src/components/AddWorker/IntegrationList.tsx
@@ -1,9 +1,5 @@
import { Button } from "@/components/ui/button";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
+import { TooltipSimple } from "@/components/ui/tooltip";
import { CircleAlert } from "lucide-react";
import { proxyFetchGet, proxyFetchPost, proxyFetchDelete } from "@/api/http";
@@ -19,7 +15,8 @@ interface IntegrationItem {
name: string;
desc: string;
env_vars: string[];
- onInstall: () => void;
+ toolkit?: string; // Add toolkit field
+ onInstall: () => void | Promise;
}
@@ -243,6 +240,8 @@ export default function IntegrationList({
}
if (installed[item.key]) return;
await item.onInstall();
+ // refresh configs after install to update installed state indicator
+ await fetchInstalled();
},
[installed]
);
@@ -302,7 +301,7 @@ export default function IntegrationList({
onConnect={onConnect}
activeMcp={activeMcp}
>
- {items.filter((item) => item.name !== "Notion").map((item) => {
+ {items.map((item) => {
const isInstalled = !!installed[item.key];
return (
-
-
+
-
-
- {item.desc}
-
-
+
{item.env_vars.length !== 0 && (
diff --git a/src/components/AddWorker/ToolSelect.tsx b/src/components/AddWorker/ToolSelect.tsx
index b6b7c362f..8a2d98652 100644
--- a/src/components/AddWorker/ToolSelect.tsx
+++ b/src/components/AddWorker/ToolSelect.tsx
@@ -7,11 +7,11 @@ import React, {
} from "react";
import { Badge } from "@/components/ui/badge";
import { CircleAlert, Store, X } from "lucide-react";
-import { proxyFetchGet, proxyFetchPost } from "@/api/http";
+import { proxyFetchGet, proxyFetchPost, fetchPost } from "@/api/http";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import githubIcon from "@/assets/github.svg";
-import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
+import { TooltipSimple } from "../ui/tooltip";
import IntegrationList from "./IntegrationList";
import { getProxyBaseURL } from "@/lib";
import { capitalizeFirstLetter } from "@/lib";
@@ -65,8 +65,73 @@ const ToolSelect = forwardRef<
.map(([key, value]: [string, any]) => {
let onInstall = null;
- onInstall = () =>
- (window.location.href = `${baseURL}/api/oauth/${key.toLowerCase()}/login`);
+ // Special handling for Notion MCP
+ if (key.toLowerCase() === 'notion') {
+ onInstall = async () => {
+ try {
+ const response = await fetchPost("/install/tool/notion");
+ if (response.success) {
+ // Save to config to mark as installed
+ await proxyFetchPost("/api/configs", {
+ config_group: "Notion",
+ config_name: "MCP_REMOTE_CONFIG_DIR",
+ config_value: response.toolkit_name || "NotionMCPToolkit",
+ });
+ console.log("Notion MCP installed successfully");
+ // After successful installation, add to selected tools
+ const notionItem = {
+ id: 0, // Use 0 for integration items
+ key: key,
+ name: key,
+ description: "Notion workspace integration for reading and managing Notion pages",
+ toolkit: "notion_mcp_toolkit", // Add the toolkit name
+ isLocal: true
+ };
+ addOption(notionItem, true);
+ } else {
+ console.error("Failed to install Notion MCP:", response.error || "Unknown error");
+ throw new Error(response.error || "Failed to install Notion MCP");
+ }
+ } catch (error: any) {
+ console.error("Failed to install Notion MCP:", error.message);
+ throw error;
+ }
+ };
+ } else if (key.toLowerCase() === 'google calendar') {
+ onInstall = async () => {
+ try {
+ const response = await fetchPost("/install/tool/google_calendar");
+ if (response.success) {
+ // Save to config to mark as installed
+ await proxyFetchPost("/api/configs", {
+ config_group: "Google Calendar",
+ config_name: "GOOGLE_CLIENT_ID",
+ config_value: response.toolkit_name || "GoogleCalendarToolkit",
+ });
+ console.log("Google Calendar installed successfully");
+ // After successful installation, add to selected tools
+ const calendarItem = {
+ id: 0, // Use 0 for integration items
+ key: key,
+ name: key,
+ description: "Google Calendar integration for managing events and schedules",
+ toolkit: "google_calendar_toolkit", // Add the toolkit name
+ isLocal: true
+ };
+ addOption(calendarItem, true);
+ } else {
+ console.error("Failed to install Google Calendar:", response.error || "Unknown error");
+ throw new Error(response.error || "Failed to install Google Calendar");
+ }
+ } catch (error: any) {
+ console.error("Failed to install Google Calendar:", error.message);
+ throw error;
+ }
+ };
+ } else {
+ onInstall = () =>
+ (window.location.href = `${baseURL}/api/oauth/${key.toLowerCase()}/login`);
+ }
return {
key: key,
@@ -78,6 +143,10 @@ const ToolSelect = forwardRef<
? `Environmental variables required: ${value.env_vars.join(
", "
)}`
+ : key.toLowerCase() === 'notion'
+ ? "Notion workspace integration for reading and managing Notion pages"
+ : key.toLowerCase() === 'google calendar'
+ ? "Google Calendar integration for managing events and schedules"
: "",
onInstall,
};
@@ -157,7 +226,7 @@ const ToolSelect = forwardRef<
envValue?: { [key: string]: any },
activeMcp?: any
) => {
- // is exa search
+ // is exa search or google calendar
if (activeMcp && envValue) {
const env: { [key: string]: string } = {};
Object.keys(envValue).map((key) => {
@@ -171,6 +240,19 @@ const ToolSelect = forwardRef<
activeMcp.install_command.env[key]
);
});
+
+ // Add to selected tools after saving config
+ if (activeMcp.key === "Google Calendar") {
+ const calendarItem = {
+ id: activeMcp.id,
+ key: activeMcp.key,
+ name: activeMcp.name,
+ description: "Google Calendar integration for managing events and schedules",
+ toolkit: "google_calendar_toolkit",
+ isLocal: true
+ };
+ addOption(calendarItem, true);
+ }
return;
// async function fetchInstalled() {
// try {
@@ -368,19 +450,12 @@ const ToolSelect = forwardRef<
{item.name}
-
-
-
+ e.stopPropagation()}
/>
-
-
-
- {item.description}
-
-
-
+
{getGithubRepoName(item.home_page) && (
@@ -434,19 +509,12 @@ const ToolSelect = forwardRef<
{item.mcp_name}
-
-
- e.stopPropagation()}
- />
-
-
-
- {item.mcp_desc}
-
-
-
+
+ e.stopPropagation()}
+ />
+
@@ -398,7 +399,7 @@ export function AddWorker({
{t("workforce.cancel")}
@@ -420,7 +421,7 @@ export function AddWorker({
{
setWorkerName(e.target.value);
@@ -430,6 +431,7 @@ export function AddWorker({
className={`!border-none !bg-transparent !shadow-none text-xl leading-2xl font-bold !ring-0 !ring-offset-0 ${
nameError ? "border-red-500" : ""
}`}
+ required
/>
{
setIsLoading(true);
if (type === "pause") {
@@ -133,6 +138,64 @@ export const BottomInput = ({
}
};
+ // drag & drop files
+ const isFileDrag = (e: React.DragEvent) => {
+ try {
+ return Array.from(e.dataTransfer?.types || []).includes("Files");
+ } catch {
+ return false;
+ }
+ };
+
+ const handleDragOver = (e: React.DragEvent) => {
+ if (!privacy || isPending || useCloudModelInDev) return;
+ if (!isFileDrag(e)) return;
+ e.preventDefault();
+ e.stopPropagation();
+ e.dataTransfer.dropEffect = "copy";
+ setIsDragging(true);
+ };
+
+ const handleDragEnter = (e: React.DragEvent) => {
+ if (!privacy || isPending || useCloudModelInDev) return;
+ if (!isFileDrag(e)) return;
+ e.preventDefault();
+ e.stopPropagation();
+ dragCounter.current += 1;
+ setIsDragging(true);
+ };
+
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ dragCounter.current = Math.max(0, dragCounter.current - 1);
+ if (dragCounter.current === 0) setIsDragging(false);
+ };
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsDragging(false);
+ dragCounter.current = 0;
+ if (!privacy || isPending || useCloudModelInDev) return;
+ try {
+ const dropped = Array.from(e.dataTransfer?.files || []);
+ if (dropped.length === 0) return;
+ const current = chatStore.tasks[chatStore.activeTaskId as string].attaches;
+ const mapped = dropped.map((f: File) => ({
+ fileName: f.name,
+ filePath: (f as any).path || f.name,
+ }));
+ const files = [
+ ...current.filter((f: File) => !mapped.find((m) => m.filePath === f.filePath)),
+ ...mapped.filter((m) => !current.find((f) => f.filePath === m.filePath)),
+ ];
+ chatStore.setAttaches(chatStore.activeTaskId as string, files as File[]);
+ } catch (error) {
+ console.error("Drop File Error:", error);
+ }
+ };
+
const handleEditQuery = () => {
fetchDelete(`/chat/${chatStore.activeTaskId}`);
const tempTaskId = chatStore.activeTaskId;
@@ -310,7 +373,22 @@ export const BottomInput = ({
)}
) : (
-
+
+ {isDragging && (
+
+
+
+ Drop files to attach
+
+
+
+ )}
)}
diff --git a/src/components/ChatBox/TaskCard.tsx b/src/components/ChatBox/TaskCard.tsx
index ff4576508..a6a2afcf8 100644
--- a/src/components/ChatBox/TaskCard.tsx
+++ b/src/components/ChatBox/TaskCard.tsx
@@ -88,7 +88,7 @@ export function TaskCard({
});
setFilterTasks(newFiltered);
}
- }, [selectedState, taskInfo]);
+ }, [selectedState, taskInfo, taskRunning]);
const isAllTaskFinished = useMemo(() => {
return (
diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx
index 2ce48b093..45d29ee6a 100644
--- a/src/components/ChatBox/index.tsx
+++ b/src/components/ChatBox/index.tsx
@@ -50,16 +50,18 @@ export default function ChatBox(): JSX.Element {
})
.catch((err) => console.error("Failed to fetch settings:", err));
- proxyFetchGet("/api/configs").then((configsRes) => {
- const configs = Array.isArray(configsRes) ? configsRes : [];
- const _hasApiKey = configs.find(
- (item) => item.config_name === "GOOGLE_API_KEY"
- );
- const _hasApiId = configs.find(
- (item) => item.config_name === "SEARCH_ENGINE_ID"
- );
- if (_hasApiKey && _hasApiId) setHasSearchKey(true);
- });
+ proxyFetchGet("/api/configs")
+ .then((configsRes) => {
+ const configs = Array.isArray(configsRes) ? configsRes : [];
+ const _hasApiKey = configs.find(
+ (item) => item.config_name === "GOOGLE_API_KEY"
+ );
+ const _hasApiId = configs.find(
+ (item) => item.config_name === "SEARCH_ENGINE_ID"
+ );
+ if (_hasApiKey && _hasApiId) setHasSearchKey(true);
+ })
+ .catch((err) => console.error("Failed to fetch configs:", err));
}, []);
// Refresh privacy status when dialog closes
diff --git a/src/components/Folder/FolderComponent.tsx b/src/components/Folder/FolderComponent.tsx
new file mode 100644
index 000000000..ceb098273
--- /dev/null
+++ b/src/components/Folder/FolderComponent.tsx
@@ -0,0 +1,112 @@
+import React, { useMemo } from "react";
+import DOMPurify from "dompurify";
+
+type Props = {
+ selectedFile: {
+ content?: string | null;
+ };
+};
+
+export default function FolderComponent({ selectedFile }: Props) {
+ const sanitizedHtml = useMemo(() => {
+ const raw = selectedFile?.content || "";
+ if (!raw) return "";
+
+ // Strict dangerous content detection to prevent various bypass techniques
+ const dangerousPatterns = [
+ /ipcRenderer/gi,
+ /window\s*\[\s*['"`]ipcRenderer['"`]\s*\]/gi,
+ /parent\s*\.\s*ipcRenderer/gi,
+ /top\s*\.\s*ipcRenderer/gi,
+ /frames\s*\[\s*\d+\s*\]\s*\.\s*ipcRenderer/gi,
+ /require\s*\(\s*['"`]electron['"`]\s*\)/gi,
+ /process\s*\.\s*versions\s*\.\s*electron/gi,
+ /nodeIntegration/gi,
+ /webSecurity/gi,
+ /contextIsolation/gi,
+ ];
+
+ for (const pattern of dangerousPatterns) {
+ if (pattern.test(raw)) {
+ console.warn("Detected forbidden content:", pattern);
+ return "";
+ }
+ }
+
+ return DOMPurify.sanitize(raw, {
+ USE_PROFILES: { html: true },
+ ALLOWED_TAGS: [
+ "a",
+ "b",
+ "i",
+ "u",
+ "strong",
+ "em",
+ "p",
+ "br",
+ "ul",
+ "ol",
+ "li",
+ "img",
+ "div",
+ "span",
+ "table",
+ "thead",
+ "tbody",
+ "tr",
+ "td",
+ "th",
+ "pre",
+ "code",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ ],
+ ALLOWED_ATTR: [
+ "href",
+ "src",
+ "alt",
+ "title",
+ "width",
+ "height",
+ "target",
+ "rel",
+ "colspan",
+ "rowspan",
+ "class",
+ "id",
+ ],
+ FORBID_ATTR: [
+ "onerror",
+ "onload",
+ "onclick",
+ "onmouseover",
+ "onfocus",
+ "onblur",
+ "onchange",
+ "onsubmit",
+ "onreset",
+ "onselect",
+ "onabort",
+ "onkeydown",
+ "onkeypress",
+ "onkeyup",
+ "onunload",
+ ],
+ FORBID_TAGS: ["script", "iframe", "object", "embed", "form", "input", "button"],
+ ADD_ATTR: ["target"],
+ SANITIZE_DOM: true,
+ KEEP_CONTENT: false,
+ });
+ }, [selectedFile?.content]);
+
+ return (
+
+ );
+}
diff --git a/src/components/Folder/index.tsx b/src/components/Folder/index.tsx
index f1a65d1a6..b670a6dfd 100644
--- a/src/components/Folder/index.tsx
+++ b/src/components/Folder/index.tsx
@@ -11,6 +11,7 @@ import {
ChevronDown,
} from "lucide-react";
import { Button } from "@/components/ui/button";
+import FolderComponent from "./FolderComponent";
import { useChatStore } from "@/store/chatStore";
import { MarkDown } from "@/components/ChatBox/MarkDown";
@@ -543,30 +544,20 @@ export default function Folder({ data }: { data?: Agent }) {
) : ["csv", "doc", "docx", "pptx", "xlsx"].includes(
selectedFile.type
) ? (
-
+
) : selectedFile.type === "html" ? (
isShowSourceCode ? (
<>{selectedFile.content}>
) : (
-
+
)
) : selectedFile.type === "zip" ? (
- {t("folder.zip-file-is-not-supported-yet")}
+
+ {t("folder.zip-file-is-not-supported-yet")}
+
) : [
@@ -609,7 +600,9 @@ export default function Folder({ data }: { data?: Agent }) {
- {t("chat.select-a-file-to-view-its-contents")}
+
+ {t("chat.select-a-file-to-view-its-contents")}
+
)}
diff --git a/src/components/InstallStep/InstallDependencies.tsx b/src/components/InstallStep/InstallDependencies.tsx
index 24b8212cf..5b913ba46 100644
--- a/src/components/InstallStep/InstallDependencies.tsx
+++ b/src/components/InstallStep/InstallDependencies.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from "react";
+import React from "react";
import { useAuthStore } from "@/store/authStore";
import { ProgressInstall } from "@/components/ui/progress-install";
import { FileDown, RefreshCcw } from "lucide-react";
@@ -14,158 +14,21 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
import { useTranslation } from "react-i18next";
+import { useInstallationUI } from "@/store/installationStore";
+import { TooltipSimple } from "../ui/tooltip";
-interface InstallLog {
- type: "stdout" | "stderr";
- data: string;
- timestamp: Date;
-}
-
-export const InstallDependencies: React.FC<{
- isInstalling: boolean;
- setIsInstalling: (isInstalling: boolean) => void;
-}> = ({ isInstalling, setIsInstalling }) => {
- const { initState, setInitState } = useAuthStore();
- const {t} = useTranslation()
- const [logs, setLogs] = useState([]);
- const [status, setStatus] = useState<
- "idle" | "installing" | "success" | "error"
- >("idle");
- const [showInstallScreen, setShowInstallScreen] = useState(true);
- const [progress, setProgress] = useState(20);
- useEffect(() => {
- // listen to install start event
- window.electronAPI.onInstallDependenciesStart(() => {
- setIsInstalling(true);
- setStatus("installing");
- setShowInstallScreen(true);
- setLogs([]);
- console.log("start installing dependencies...");
- setProgress(20);
- });
-
- // listen to install log
- window.electronAPI.onInstallDependenciesLog(
- (data: { type: string; data: string }) => {
- console.log("data", data);
- const newLog: InstallLog = {
- type: data?.type as "stdout" | "stderr",
- data: data?.data,
- timestamp: new Date(),
- };
- setProgress((prev) => {
- const progress = prev + 5;
- if (progress >= 90) {
- return 90;
- }
- return progress;
- });
- console.log(`install log [${data?.type}]:`, data?.data);
- setLogs((prev) => [...prev, newLog]);
- }
- );
-
- // listen to install complete event
- window.electronAPI.onInstallDependenciesComplete(
- (data: { success: boolean; code?: number; error?: string }) => {
- setIsInstalling(false);
- if (data?.success) {
- setStatus("success");
- console.log("dependencies installed successfully!");
- setProgress(100);
- setInitState("done");
- } else {
- setStatus("error");
- console.error("dependencies installation failed:", data?.code);
- console.error("dependencies installation failed:", data?.error);
- }
- }
- );
-
- // after component mounted, notify main process frontend is ready
- const notifyFrontendReady = async () => {
- try {
- // check if there is frontend-ready API
- if (window.electronAPI.frontendReady) {
- await window.electronAPI.frontendReady();
- }
- } catch (error) {
- console.log(
- "frontend ready notification failed, maybe manual install mode:",
- error
- );
- }
- };
-
- // delay notification, ensure component is fully initialized
- setTimeout(notifyFrontendReady, 500);
-
- // clean up listeners
- return () => {
- window.electronAPI.removeAllListeners("install-dependencies-start");
- window.electronAPI.removeAllListeners("install-dependencies-log");
- window.electronAPI.removeAllListeners("install-dependencies-complete");
- };
- }, []);
-
- const handleInstall = async () => {
- try {
- setStatus("installing");
- setIsInstalling(true);
- setLogs([]);
- setShowInstallScreen(true);
-
- const result = await window.electronAPI.installDependencies();
- console.log("result", result);
- if (!result.success) {
- setStatus("error");
- setIsInstalling(false);
- return;
- }
- setStatus("success");
- setProgress(100);
- setIsInstalling(false);
- setInitState("done");
- } catch (error) {
- console.error("install start failed:", error);
- setStatus("error");
- setIsInstalling(false);
- }
- };
-
- const exportLog = async () => {
- try {
- const response = await window.electronAPI.exportLog();
-
- if (!response.success) {
- alert("Export cancelled:" + response.error);
- return;
- }
- if (response.savedPath) {
- window.location.href =
- "https://github.com/eigent-ai/eigent/issues/new/choose";
- alert("log saved:" + response.savedPath);
- }
- } catch (e: any) {
- alert("export error:" + e.message);
- }
- };
-
- // if not show install interface, return null
- if (initState === "done" && !isInstalling) {
- return (
-
- );
- }
+export const InstallDependencies: React.FC = () => {
+ const { initState } = useAuthStore();
+ const {t} = useTranslation();
+
+ const {
+ progress,
+ latestLog,
+ error,
+ isInstalling,
+ retryInstallation,
+ exportLog,
+ } = useInstallationUI();
return (
@@ -180,16 +43,18 @@ export const InstallDependencies: React.FC<{
{isInstalling ? "System Installing ..." : ""}
- {logs.at(-1)?.data}
+ {latestLog?.data}
-
-
-
+
+
+
+
+
@@ -214,7 +79,7 @@ export const InstallDependencies: React.FC<{
{t("layout.report-bug")}
-
+
{t("layout.retry")}
diff --git a/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx b/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx
new file mode 100644
index 000000000..bdfd2c1a0
--- /dev/null
+++ b/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx
@@ -0,0 +1,50 @@
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { error } from "electron-log";
+import { t } from "i18next";
+import React from "react";
+
+interface InstallationErrorDialogProps {
+ error: string;
+ installationState: string;
+ latestLog: any;
+ retryInstallation: () => void;
+}
+
+const InstallationErrorDialog = ({
+ error,
+ installationState,
+ latestLog,
+ retryInstallation,
+}:InstallationErrorDialogProps) => {
+ return (
+
+ );
+};
+
+export default InstallationErrorDialog;
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index 685531bfe..173acdddd 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -8,17 +8,48 @@ import { AnimationJson } from "@/components/AnimationJson";
import animationData from "@/assets/animation/onboarding_success.json";
import CloseNoticeDialog from "../Dialog/CloseNotice";
import { useChatStore } from "@/store/chatStore";
+import { useInstallationUI } from "@/store/installationStore";
+import { useInstallationSetup } from "@/hooks/useInstallationSetup";
+import InstallationErrorDialog from "../InstallStep/InstallationErrorDialog/InstallationErrorDialog";
const Layout = () => {
- const { initState, setInitState, isFirstLaunch, setIsFirstLaunch } =
- useAuthStore();
- const [isInstalling, setIsInstalling] = useState(false);
+ const { initState, isFirstLaunch, setIsFirstLaunch, setInitState } = useAuthStore();
const [noticeOpen, setNoticeOpen] = useState(false);
const chatStore = useChatStore();
-
+ const {
+ installationState,
+ latestLog,
+ error,
+ isInstalling,
+ shouldShowInstallScreen,
+ retryInstallation,
+ } = useInstallationUI();
+
+ // Setup installation IPC listeners and state synchronization
+ useInstallationSetup();
+
+ // Additional check: If initState is carousel but tools are installed, skip to done
+ useEffect(() => {
+ const checkAndSkipCarousel = async () => {
+ if (initState === 'carousel' && !isInstalling) {
+ try {
+ const result = await window.ipcRenderer.invoke("check-tool-installed");
+ if (result.success && result.isInstalled) {
+ console.log('[Layout] Tools installed, skipping carousel and setting initState to done');
+ setInitState('done');
+ }
+ } catch (error) {
+ console.error('[Layout] Failed to check tool installation:', error);
+ }
+ }
+ };
+
+ checkAndSkipCarousel();
+ }, [initState, isInstalling, setInitState]);
+
useEffect(() => {
const handleBeforeClose = () => {
const currentStatus = chatStore.tasks[chatStore.activeTaskId as string]?.status;
- if(["pending", "running", "pause"].includes(currentStatus)) {
+ if(["running", "pause"].includes(currentStatus)) {
setNoticeOpen(true);
} else {
window.electronAPI.closeWindow(true);
@@ -32,43 +63,46 @@ const Layout = () => {
};
}, [chatStore.tasks, chatStore.activeTaskId]);
- useEffect(() => {
- const checkToolInstalled = async () => {
- // in render process
- const result = await window.ipcRenderer.invoke("check-tool-installed");
- if (result.success) {
- if (initState === "done" && !result.isInstalled) {
- setInitState("carousel");
- }
- console.log("tool is installed:");
- } else {
- console.error("check failed:", result.error);
- }
- };
- checkToolInstalled();
- }, []);
+ // Determine what to show based on states
+ const shouldShowOnboarding = initState === "done" && isFirstLaunch && !isInstalling;
+ // Show install screen if either:
+ // 1. The installation store says to show it (isVisible && not completed)
+ // 2. OR if initState is not 'done' (meaning permissions or carousel should show)
+ const actualShouldShowInstallScreen = shouldShowInstallScreen || initState !== 'done';
+ const shouldShowMainContent = !actualShouldShowInstallScreen;
return (
- {initState === "done" && isFirstLaunch && !isInstalling && (
+ {/* Onboarding animation */}
+ {shouldShowOnboarding && (
{
- setIsFirstLaunch(false);
- }}
+ onComplete={() => setIsFirstLaunch(false)}
animationData={animationData}
/>
)}
- {(initState !== "done" || isInstalling) && (
-
+
+ {/* Installation screen */}
+ {actualShouldShowInstallScreen && }
+
+ {/* Main app content */}
+ {shouldShowMainContent && (
+ <>
+
+
+ >
)}
-
-
+
+ {(error != "" && error !=undefined) &&
+
+ }
+
- {t("chat.you-ve-reached-the-limit-of-your-current-plan")}
+ {i18n.t("chat.you-ve-reached-the-limit-of-your-current-plan")}
(window.location.href = "https://www.eigent.ai/pricing")}
>
- {t("chat.upgrade")}
+ {i18n.t("chat.upgrade")}
{" "}
- {t("chat.your-account-or-switch-to-a-self-hosted-model-and-api-in")}{" "}
+ {i18n.t("chat.your-account-or-switch-to-a-self-hosted-model-and-api-in")}{" "}
(window.location.href = "#/setting/general")}
>
- {t("chat.settings")}
+ {i18n.t("chat.settings")}
{" "}
.
,
diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx
index f9f8a3343..c8f8e100d 100644
--- a/src/components/TopBar/index.tsx
+++ b/src/components/TopBar/index.tsx
@@ -18,8 +18,10 @@ import { useSidebarStore } from "@/store/sidebarStore";
import chevron_left from "@/assets/chevron_left.svg";
import { getAuthStore } from "@/store/authStore";
import { useTranslation } from "react-i18next";
+import { proxyFetchGet } from "@/api/http";
+import { toast } from "sonner";
function HeaderWin() {
- const {t} = useTranslation();
+ const { t } = useTranslation();
const titlebarRef = useRef(null);
const controlsRef = useRef(null);
const [platform, setPlatform] = useState("");
@@ -110,6 +112,22 @@ function HeaderWin() {
chatStore.tasks[chatStore.activeTaskId as string]?.summaryTask,
]);
+ const getReferFriendsLink = async () => {
+ try {
+ const res: any = await proxyFetchGet("/api/user/invite_code");
+ if (res?.invite_code) {
+ const inviteLink = `https://www.eigent.ai/signup?invite_code=${res.invite_code}`;
+ await navigator.clipboard.writeText(inviteLink);
+ toast.success("Invitation link copied!");
+ } else {
+ toast.error("Failed to get invite code");
+ }
+ } catch (error) {
+ console.error("Failed to get referral link:", error);
+ toast.error("Failed to get invitation link");
+ }
+ };
+
return (
{location.pathname !== "/history" && (
<>
- {activeTaskTitle === "New Project" ? (
+ {activeTaskTitle === t("chat.new-project") ? (
) : (
- {t("chat.new-project")}
+ {activeTaskTitle}
)}
>
@@ -185,9 +203,7 @@ function HeaderWin() {
{t("layout.report-bug")}
{
- window.location.href = "https://www.eigent.ai/dashboard";
- }}
+ onClick={getReferFriendsLink}
variant="primary"
size="xs"
className="no-drag text-button-primary-text-default leading-tight"
@@ -239,4 +255,4 @@ function HeaderWin() {
);
}
-export default HeaderWin;
+export default HeaderWin;
\ No newline at end of file
diff --git a/src/components/WorkFlow/index.tsx b/src/components/WorkFlow/index.tsx
index b0b6bf3c0..9f0bdc564 100644
--- a/src/components/WorkFlow/index.tsx
+++ b/src/components/WorkFlow/index.tsx
@@ -379,7 +379,7 @@ export default function Workflow({
handleShare(chatStore.activeTaskId as string);
}}
>
- Share {t("workforce.share")}
+ {t("workforce.share")}
)}
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
index 13c4e7827..b74630b77 100644
--- a/src/components/ui/tooltip.tsx
+++ b/src/components/ui/tooltip.tsx
@@ -27,4 +27,41 @@ const TooltipContent = React.forwardRef<
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
-export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+/**
+ * A simpler interface for Tooltip when you just need a trigger and content.
+ *
+ * Usage:
+ * ```jsx
+ *
+ * Hover me
+ *
+ * ```
+ */
+interface TooltipSimpleProps extends Omit, 'children' | 'content'> {
+ children: React.ReactNode;
+ content: React.ReactNode;
+}
+
+const TooltipSimple = React.forwardRef<
+ React.ElementRef,
+ TooltipSimpleProps
+>(({ children, content, className, sideOffset = 4, ...props }, ref) => {
+ return (
+
+
+
+ {children}
+
+
+
+ {content}
+
+
+
+ );
+})
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, TooltipSimple }
diff --git a/src/hooks/useInstallationSetup.ts b/src/hooks/useInstallationSetup.ts
new file mode 100644
index 000000000..16e19275d
--- /dev/null
+++ b/src/hooks/useInstallationSetup.ts
@@ -0,0 +1,102 @@
+import { useEffect } from 'react';
+import { useInstallationStore } from '@/store/installationStore';
+import { useAuthStore } from '@/store/authStore';
+
+/**
+ * Hook that sets up Electron IPC listeners and handles installation state synchronization
+ * This should be called once in your App component or Layout component
+ */
+export const useInstallationSetup = () => {
+ const { initState, setInitState } = useAuthStore();
+
+ // Extract only the functions we need to avoid dependency issues
+ const startInstallation = useInstallationStore(state => state.startInstallation);
+ const addLog = useInstallationStore(state => state.addLog);
+ const setSuccess = useInstallationStore(state => state.setSuccess);
+ const setError = useInstallationStore(state => state.setError);
+
+ // Check tool installation status on mount
+ useEffect(() => {
+ const checkToolInstalled = async () => {
+ try {
+ console.log('[useInstallationSetup] Checking tool installation status...');
+ const result = await window.ipcRenderer.invoke("check-tool-installed");
+
+ console.log('[useInstallationSetup] Tool check result:', result, 'initState:', initState);
+
+ // If tools are NOT installed and we're in done state, go back to carousel
+ if (result.success && initState === "done" && !result.isInstalled) {
+ console.log('[useInstallationSetup] Tool not installed, setting initState to carousel');
+ setInitState("carousel");
+ }
+
+ // If tools ARE installed and we're in carousel state, go to done
+ if (result.success && initState === "carousel" && result.isInstalled) {
+ console.log('[useInstallationSetup] Tools installed but initState is carousel, setting to done');
+ setInitState("done");
+ }
+ } catch (error) {
+ console.error("[useInstallationSetup] Tool installation check failed:", error);
+ }
+ };
+
+ const checkBackendStatus = async() => {
+ try {
+ // Also check if installation is currently in progress
+ const installationStatus = await window.electronAPI.getInstallationStatus();
+ console.log('[useInstallationSetup] Installation status check:', installationStatus);
+
+ if (installationStatus.success && installationStatus.isInstalling) {
+ console.log('[useInstallationSetup] Installation in progress, starting frontend state');
+ startInstallation();
+ }
+ } catch (err) {
+ console.error('[useInstallationSetup] Failed to check installation status:', err);
+ }
+ }
+
+ checkToolInstalled();
+ checkBackendStatus();
+ }, [initState, setInitState, startInstallation]);
+
+ // Setup Electron IPC listeners (only once)
+ useEffect(() => {
+ // Electron IPC event handlers
+ const handleInstallStart = () => {
+ startInstallation();
+ };
+
+ const handleInstallLog = (data: { type: string; data: string }) => {
+ addLog({
+ type: data.type as 'stdout' | 'stderr',
+ data: data.data,
+ timestamp: new Date(),
+ });
+ };
+
+ const handleInstallComplete = (data: { success: boolean; code?: number; error?: string }) => {
+ console.log('[useInstallationSetup] Install complete event received:', data);
+
+ if (data.success) {
+ setSuccess();
+ setInitState('done');
+ } else {
+ setError(data.error || 'Installation failed');
+ }
+ };
+
+ // Register Electron IPC listeners
+ window.electronAPI.onInstallDependenciesStart(handleInstallStart);
+ window.electronAPI.onInstallDependenciesLog(handleInstallLog);
+ window.electronAPI.onInstallDependenciesComplete(handleInstallComplete);
+
+ console.log('[useInstallationSetup] Installation listeners registered');
+
+ // Cleanup listeners on unmount
+ return () => {
+ window.electronAPI.removeAllListeners('install-dependencies-start');
+ window.electronAPI.removeAllListeners('install-dependencies-log');
+ window.electronAPI.removeAllListeners('install-dependencies-complete');
+ };
+ }, [startInstallation, addLog, setSuccess, setError, setInitState]);
+};
\ No newline at end of file
diff --git a/src/i18n/locales/ar/chat.json b/src/i18n/locales/ar/chat.json
index 0e2c07e5c..9c44757ba 100644
--- a/src/i18n/locales/ar/chat.json
+++ b/src/i18n/locales/ar/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": ".أنت في وضع الاستضافة الذاتية. لا يمكن استخدام النماذج السحابية هنا - قم بإعداد نموذج سحابي محلي خاص بك للحفاظ على سير الأمور",
"you-are-using-self-hosted-mode-mcp": ".أنت تستخدم وضع الاستضافة الذاتية. أدخل مفاتيح بحث Google في \"MCP والأدوات\" لضمان عمل Eigent بشكل صحيح",
"palm-springs-tennis-trip-planner": "مخطط رحلة تنس في بالم سبرينغس",
- "palm-springs-tennis-trip-planner-message": ".نحن من عشاق التنس ونرغب في حضور بطولة تنس في بالم سبرينغز. أعيش في سان فرانسيسكو. يُرجى إعداد برنامج رحلة مفصل يتضمن الرحلات الجوية والفنادق والأنشطة التي يمكن القيام بها لمدة 3 أيام - تقريبًا عند موعد نصف النهائي/النهائي. نحب المشي لمسافات طويلة والطعام النباتي والمنتجعات الصحية. ميزانيتنا 5000 دولار أمريكي. يجب أن يتضمن برنامج الرحلة جدولًا زمنيًا مفصلاً للوقت والأنشطة والتكلفة وتفاصيل أخرى، وإذا أمكن، رابطًا لشراء التذاكر/الحجز، وما إلى ذلك لهذا العنصر. بعض التفضيلات: 1. الدخول إلى المنتجع الصحي سيكون جيدًا ولكنه ليس ضروريًا. 2. عند الانتهاء من هذه المهمة، يُرجى إنشاء تقرير هتمل حول هذه الرحلة",
+ "palm-springs-tennis-trip-planner-message": "أنا من محبي التنس وأرغب في مشاهدة بطولة التنس في بالم سبرينغز. أعيش في سان فرانسيسكو (SF). يرجى إعداد خط سير مفصل لمدة ثلاثة أيام مع رحلات الطيران والفنادق والأنشطة للفترة المحيطة بنصف النهائي/النهائي. أحب التنزه والطعام النباتي والمنتجعات الصحية. ميزانيتي هي 5000 دولار. يجب أن يكون خط السير جدولًا زمنيًا مفصلاً يوضح الأوقات والأنشطة والتكاليف وتفاصيل أخرى، وحيثما ينطبق، روابط لشراء التذاكر أو إجراء الحجوزات. التفضيلات: 1. سيكون الوصول إلى منتجع صحي أمرًا لطيفًا ولكنه ليس ضروريًا. 2. عند الانتهاء من هذه المهمة، يرجى إنشاء تقرير HTML حول الرحلة.",
"bank-transfer-csv-analysis-and-visualization": "تحليل سيسفي والتصور للتحويل البنكي",
"bank-transfer-csv-analysis-and-visualization-message": ".تفضل بزيارة ملف سيسفي تجريبي لنماذج بنكية يحتوي على ١٠ أفكار و ١٠ صفوف. اقرأ ملف سيسفي الناتج ولخّص البيانات. تتوفر رسوم بيانية لعرض الاتجاهات أو الرؤى ذات الصلة من البيانات",
"find-duplicate-files-in-downloads-folder": "رسالة لبحث عن الملفات المكررة في مجلد التنزيلات",
diff --git a/src/i18n/locales/ar/setting.json b/src/i18n/locales/ar/setting.json
index c473c2e24..1aae7b2e0 100644
--- a/src/i18n/locales/ar/setting.json
+++ b/src/i18n/locales/ar/setting.json
@@ -20,7 +20,7 @@
"data-privacy-description": "يعتمد أيجنت على الوضع المحلي الجديد ويضمن خصوصيتك. تبقى بياناتك على جهازك افتراضيًا. أذونات السحابة اختيارية، ويُستخدم حد البيانات لأغراض العمل فقط",
"privacy-policy": "سياسة الخصوصية",
"how-we-handle-your-data": "كيف نتعامل مع بياناتك",
- "how-we-handle-your-data-line-1": ":نحن نستخدم فقط البيانات الأساسية اللازمة لتنفيذ مهامك",
+ "how-we-handle-your-data-line-1": "نحن نستخدم فقط البيانات الأساسية اللازمة لتنفيذ مهامك",
"how-we-handle-your-data-line-1-line-1": ".يمكنك اختيار لقطات شاشة أيجنت لتحليل عناصر واجهة المستخدم وقراءة النص وتحديد الإجراء التالي، تمامًا كما تفعل",
"how-we-handle-your-data-line-1-line-2": ".يمكن استخدام الماوس ولوحة المفاتيح المحلية للوصول إلى البرامج والملفات المحلية التي تم تنشيطها",
"how-we-handle-your-data-line-1-line-3": "لا يتم توفير سوى الحد الأدنى من البيانات ذات الصلة لنماذج الذكاء الاصطناعي أو عمليات التكامل مع جهات خارجية التي تمكنها؛ وليس لدينا أي احتفاظ بالبيانات",
@@ -89,7 +89,7 @@
"coming-soon": "قريبًا",
"uninstall": "إلغاء التثبيت",
"install": "تثبيت",
- "add-your-mcp-server": "الخاص بك MCP أضف خادم",
+ "add-your-agent": "أضف وكيلك",
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": ".أضف مضيف مسيبي محليًا عن طريق توفير جسون صارم صالح",
"learn-more": "اتعلم اكثر",
"installing": "جارٍ التثبيت...",
diff --git a/src/i18n/locales/ar/workforce.json b/src/i18n/locales/ar/workforce.json
index 4837def4d..b8390872e 100644
--- a/src/i18n/locales/ar/workforce.json
+++ b/src/i18n/locales/ar/workforce.json
@@ -3,7 +3,7 @@
"new-worker": "عامل جديد",
"edit": "تعديل",
"configure-mcp-server": "تكوين خادم مسيبي",
- "add-your-mcp-server": "إضافة خادم مسيبي",
+ "add-your-agent": "إضافة خادم مسيبي",
"cancel": "إلغاء",
"save-changes": "حفظ التغييرات",
"description-optional": "الوصف (اختياري)",
diff --git a/src/i18n/locales/de/chat.json b/src/i18n/locales/de/chat.json
index f4bfd3dab..78021dacd 100644
--- a/src/i18n/locales/de/chat.json
+++ b/src/i18n/locales/de/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "Sie befinden sich im Self-hosted-Modus. Cloud-Modelle können hier nicht verwendet werden – richten Sie Ihr eigenes lokales Cloud-Modell ein, um den Betrieb aufrechtzuerhalten.",
"you-are-using-self-hosted-mode-mcp": "Sie verwenden den Self-hosted-Modus. Geben Sie die Google Search Keys in „MCP und Tools“ ein, um sicherzustellen, dass Eigent ordnungsgemäß funktioniert.",
"palm-springs-tennis-trip-planner": "Palm Springs Tennis-Reiseplaner",
- "palm-springs-tennis-trip-planner-message": "Wir sind zwei Tennis-Fans und möchten das Tennisturnier in Palm Springs besuchen. Ich wohne in SF – bitte erstellen Sie einen detaillierten Reiseplan mit Flügen, Hotels und Aktivitäten für 3 Tage – ungefähr zur Zeit der Halbfinal-/Finalspiele. Wir mögen Wandern, veganes Essen und Spas. Unser Budget beträgt 5.000 $. Der Reiseplan sollte eine detaillierte Zeitleiste mit Uhrzeit, Aktivität, Kosten, weiteren Details und, falls zutreffend, einem Link zum Kauf von Tickets/Reservierungen usw. für den jeweiligen Punkt enthalten. Einige Präferenzen: 1. Spa-Zugang wäre schön, ist aber nicht notwendig. 2. Wenn Sie diese Aufgabe abgeschlossen haben, erstellen Sie bitte einen HTML-Bericht über diese Reise.",
+ "palm-springs-tennis-trip-planner-message": "Ich bin ein Tennisfan und möchte das Tennisturnier in Palm Springs sehen. Ich lebe in San Francisco (SF). Bitte erstellen Sie einen detaillierten dreitägigen Reiseplan mit Flügen, Hotels und Aktivitäten für den Zeitraum um die Halbfinals/Finals. Ich mag Wandern, veganes Essen und Spas. Mein Budget beträgt 5.000 $. Der Reiseplan sollte ein detaillierter Zeitplan sein, der Zeiten, Aktivitäten, Kosten und andere Details zeigt und – wo zutreffend – Links zum Kauf von Tickets oder zur Vornahme von Reservierungen enthält. Vorlieben: 1. Zugang zu einem Spa wäre schön, ist aber nicht notwendig. 2. Wenn Sie diese Aufgabe abgeschlossen haben, erstellen Sie bitte einen HTML-Bericht über die Reise.",
"bank-transfer-csv-analysis-and-visualization": "Banküberweisung CSV-Analyse und Visualisierung",
"bank-transfer-csv-analysis-and-visualization-message": "Erstellen Sie eine Mock-CSV-Datei für Banküberweisungen mit 10 Spalten und 10 Zeilen. Lesen Sie die generierte CSV-Datei und fassen Sie die Daten zusammen. Erstellen Sie ein Diagramm, um relevante Trends oder Erkenntnisse aus den Daten zu visualisieren.",
"find-duplicate-files-in-downloads-folder": "Doppelte Dateien im Download-Ordner finden",
diff --git a/src/i18n/locales/de/setting.json b/src/i18n/locales/de/setting.json
index 421f43b29..92e7bc10a 100644
--- a/src/i18n/locales/de/setting.json
+++ b/src/i18n/locales/de/setting.json
@@ -20,7 +20,7 @@
"data-privacy-description": "Eigent basiert auf einem Local-First-Prinzip, um Ihre Privatsphäre zu gewährleisten. Ihre Daten verbleiben standardmäßig auf Ihrem Gerät. Cloud-Funktionen sind optional und verwenden nur die minimal erforderlichen Daten, um zu funktionieren. Für vollständige Details besuchen Sie bitte unsere",
"privacy-policy": "Datenschutzrichtlinie",
"how-we-handle-your-data": "Wie wir Ihre Daten behandeln",
- "how-we-handle-your-data-line-1": "Wir verwenden nur die für die Ausführung Ihrer Aufgaben erforderlichen Daten:",
+ "how-we-handle-your-data-line-1": "Wir verwenden nur die für die Ausführung Ihrer Aufgaben erforderlichen Daten",
"how-we-handle-your-data-line-1-line-1": "Eigent kann Screenshots erfassen, um UI-Elemente zu analysieren, Text zu lesen und die nächste Aktion zu bestimmen, genau wie Sie es tun würden.",
"how-we-handle-your-data-line-1-line-2": "Eigent kann Ihre Maus und Tastatur verwenden, um auf von Ihnen angegebene lokale Software und Dateien zuzugreifen.",
"how-we-handle-your-data-line-1-line-3": "Nur minimale Aufgabendaten werden an KI-Modellanbieter oder von Ihnen aktivierte Drittanbieter-Integrationen gesendet; wir speichern keine Daten.",
@@ -89,7 +89,7 @@
"coming-soon": "Demnächst verfügbar",
"uninstall": "Deinstallieren",
"install": "Installieren",
- "add-your-mcp-server": "Fügen Sie Ihren MCP-Server hinzu",
+ "add-your-agent": "Ihren Agenten hinzufügen",
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Fügen Sie einen lokalen MCP-Server hinzu, indem Sie eine gültige JSON-Konfiguration bereitstellen.",
"learn-more": "Mehr erfahren",
"installing": "Wird installiert...",
diff --git a/src/i18n/locales/de/workforce.json b/src/i18n/locales/de/workforce.json
index a3640a5e6..74acdc2a5 100644
--- a/src/i18n/locales/de/workforce.json
+++ b/src/i18n/locales/de/workforce.json
@@ -3,7 +3,7 @@
"new-worker": "Neuer Mitarbeiter",
"edit": "Bearbeiten",
"configure-mcp-server": "MCP-Server konfigurieren",
- "add-your-mcp-server": "Fügen Sie Ihren MCP-Server hinzu",
+ "add-your-agent": "Ihren Agenten hinzufügen",
"cancel": "Abbrechen",
"save-changes": "Änderungen speichern",
"description-optional": "Beschreibung (Optional)",
diff --git a/src/i18n/locales/en-us/chat.json b/src/i18n/locales/en-us/chat.json
index a24a048fa..b9ac851ae 100644
--- a/src/i18n/locales/en-us/chat.json
+++ b/src/i18n/locales/en-us/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "You're in Self-hosted mode. Cloud models can't be used here — set up your own local cloud model to keep things running.",
"you-are-using-self-hosted-mode-mcp": "You're using Self-hosted mode. Enter the Google Search Keys in “MCP and Tools” to ensure Eigent works properly.",
"palm-springs-tennis-trip-planner": "Palm Springs Tennis Trip Planner",
- "palm-springs-tennis-trip-planner-message": "I am two tennis fans and want to go see the tennis tournament in palm springs. l live in SF - please prepare a detailed itinerary with flights, hotels, things to do for 3 days - around the time semifinal/finals are happening. We like hiking, vegan food and spas. Our budget is $5K. The itinerary should be a detailed timeline of time, activity, cost, other details and if applicable a link to buy tickets/make reservations etc. for the item. Some preferences 1.Spa access would be nice but not necessary 2. When you finnish this task, please generate a html report about this trip.",
+ "palm-springs-tennis-trip-planner-message": "I am a tennis fan and want to see the tennis tournament in Palm Springs. I live in San Francisco (SF). Please prepare a detailed, three-day itinerary with flights, hotels, and activities for the period around the semifinals/finals. I like hiking, vegan food, and spas. My budget is $5,000. The itinerary should be a detailed timeline showing times, activities, costs, and other details, and — where applicable — links to buy tickets or make reservations. Preferences: 1. Spa access would be nice but is not necessary. 2. When you finish this task, please generate an HTML report about the trip.",
"bank-transfer-csv-analysis-and-visualization": "Bank Transfer CSV Analysis and Visualization",
"bank-transfer-csv-analysis-and-visualization-message": "Create a mock bank transfer CSV file include 10 columns and 10 rows. Read the generated CSV file and summarize the data, generate a chart to visualize relevant trends or insights from the data.",
"find-duplicate-files-in-downloads-folder": "Find Duplicate Files in Downloads Folder",
diff --git a/src/i18n/locales/en-us/setting.json b/src/i18n/locales/en-us/setting.json
index 7c2be41dc..c9a4086b5 100644
--- a/src/i18n/locales/en-us/setting.json
+++ b/src/i18n/locales/en-us/setting.json
@@ -20,7 +20,7 @@
"data-privacy-description": "Eigent is built on a local-first principle to ensure your privacy. Your data remains on your device by default. Cloud features are optional and only use the minimum data necessary to function. For full details, visit our",
"privacy-policy": "Privacy Policy",
"how-we-handle-your-data": "How we handle your data",
- "how-we-handle-your-data-line-1": "We only use the essential data needed to run your tasks:",
+ "how-we-handle-your-data-line-1": "We only use the essential data needed to run your tasks",
"how-we-handle-your-data-line-1-line-1": "Eigent may capture screenshots to analyze UI elements, read text, and determine the next action, just as you would.",
"how-we-handle-your-data-line-1-line-2": "Eigent may use your mouse and keyboard to access local software and files you specify.",
"how-we-handle-your-data-line-1-line-3": "Only the minimum task data is sent to AI model providers or the third-party integrations you enable; we have zero data-retention",
@@ -89,7 +89,7 @@
"coming-soon": "Coming Soon",
"uninstall": "Uninstall",
"install": "Install",
- "add-your-mcp-server": "Add your MCP server",
+ "add-your-agent": "Add Your Agent",
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Add a local MCP server by providing a valid JSON configuration.",
"learn-more": "Learn more",
"installing": "Installing...",
diff --git a/src/i18n/locales/en-us/workforce.json b/src/i18n/locales/en-us/workforce.json
index b329beb2d..032befce1 100644
--- a/src/i18n/locales/en-us/workforce.json
+++ b/src/i18n/locales/en-us/workforce.json
@@ -1,9 +1,9 @@
{
"your-ai-workforce": "Your AI Workforce",
- "new-worker": "New Worker",
+ "new-worker": "New Worker Agent",
"edit": "Edit",
"configure-mcp-server": "Configure MCP Server",
- "add-your-mcp-server": "Add Your MCP Server",
+ "add-your-agent": "Add Your Agent",
"cancel": "Cancel",
"save-changes": "Save changes",
"description-optional": "Description (Optional)",
diff --git a/src/i18n/locales/es/chat.json b/src/i18n/locales/es/chat.json
index d8bab2787..b855733a5 100644
--- a/src/i18n/locales/es/chat.json
+++ b/src/i18n/locales/es/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "Estás en modo autohospedado. No se pueden usar modelos en la nube aquí; configura tu propio modelo en la nube local para que todo siga funcionando.",
"you-are-using-self-hosted-mode-mcp": "Estás usando el modo autohospedado. Ingresa las claves de búsqueda de Google en “MCP y herramientas” para asegurar que Eigent funcione correctamente.",
"palm-springs-tennis-trip-planner": "Planificador de viaje de tenis a Palm Springs",
- "palm-springs-tennis-trip-planner-message": "Somos dos aficionados al tenis y queremos ir a ver el torneo en Palm Springs. Vivo en San Francisco; por favor, prepara un itinerario detallado con vuelos, hoteles y actividades para 3 días, alrededor de las semifinales/finales. Nos gustan el senderismo, la comida vegana y los spas. Nuestro presupuesto es de 5.000 dólares. El itinerario debe ser una línea de tiempo detallada con hora, actividad, costo, otros detalles y, si aplica, un enlace para comprar entradas/hacer reservas, etc. Preferencias: 1) Acceso a spa sería agradable pero no imprescindible. 2) Cuando termines esta tarea, genera un informe HTML sobre este viaje.",
+ "palm-springs-tennis-trip-planner-message": "Soy un aficionado al tenis y quiero ver el torneo de tenis en Palm Springs. Vivo en San Francisco (SF). Por favor, prepare un itinerario detallado de tres días con vuelos, hoteles y actividades para el período de las semifinales/finales. Me gusta el senderismo, la comida vegana y los spas. Mi presupuesto es de 5,000 $. El itinerario debe ser un cronograma detallado que muestre horarios, actividades, costos y otros detalles, y, cuando corresponda, enlaces para comprar boletos o hacer reservaciones. Preferencias: 1. El acceso a un spa estaría bien, pero no es necesario. 2. Cuando termine esta tarea, por favor, genere un informe en HTML sobre el viaje.",
"bank-transfer-csv-analysis-and-visualization": "Análisis y visualización de transferencias bancarias en CSV",
"bank-transfer-csv-analysis-and-visualization-message": "Crea un archivo CSV simulado de transferencias bancarias que incluya 10 columnas y 10 filas. Lee el CSV generado y resume los datos; genera un gráfico para visualizar tendencias o ideas relevantes a partir de los datos.",
"find-duplicate-files-in-downloads-folder": "Encontrar archivos duplicados en la carpeta Descargas",
diff --git a/src/i18n/locales/es/setting.json b/src/i18n/locales/es/setting.json
index a6bf14b5d..f0e206267 100644
--- a/src/i18n/locales/es/setting.json
+++ b/src/i18n/locales/es/setting.json
@@ -20,7 +20,7 @@
"data-privacy-description": "Eigent está construido sobre un principio local-first para garantizar tu privacidad. Tus datos permanecen en tu dispositivo por defecto. Las características en la nube son opcionales y solo utilizan los datos mínimos necesarios para funcionar. Para obtener más detalles, visita nuestra",
"privacy-policy": "Política de privacidad",
"how-we-handle-your-data": "Cómo manejamos tus datos",
- "how-we-handle-your-data-line-1": "Solo utilizamos los datos esenciales necesarios para ejecutar tus tareas:",
+ "how-we-handle-your-data-line-1": "Solo utilizamos los datos esenciales necesarios para ejecutar tus tareas",
"how-we-handle-your-data-line-1-line-1": "Eigent puede capturar capturas de pantalla para analizar elementos de la interfaz de usuario, leer texto y determinar la siguiente acción, como tú.",
"how-we-handle-your-data-line-1-line-2": "Eigent puede usar tu mouse y teclado para acceder a software y archivos locales que especifiques.",
"how-we-handle-your-data-line-1-line-3": "Solo se envían los datos mínimos de las tareas a proveedores de modelos de IA o a las integraciones de terceros que activas; no tenemos ninguna retención de datos",
@@ -89,7 +89,7 @@
"coming-soon": "Próximamente",
"uninstall": "Desinstalar",
"install": "Instalar",
- "add-your-mcp-server": "Agregar tu servidor MCP",
+ "add-your-agent": "Añade tu Agente",
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Agregar un servidor MCP local proporcionando una configuración JSON válida.",
"learn-more": "Aprender más",
"installing": "Instalando...",
diff --git a/src/i18n/locales/es/workforce.json b/src/i18n/locales/es/workforce.json
index aac671bdf..d086f2350 100644
--- a/src/i18n/locales/es/workforce.json
+++ b/src/i18n/locales/es/workforce.json
@@ -3,7 +3,7 @@
"new-worker": "Nuevo Worker",
"edit": "Editar",
"configure-mcp-server": "Configurar MCP Server",
- "add-your-mcp-server": "Agregar tu MCP Server",
+ "add-your-agent": "Añade tu Agente",
"cancel": "Cancelar",
"save-changes": "Guardar cambios",
"description-optional": "Descripción (Opcional)",
diff --git a/src/i18n/locales/fr/chat.json b/src/i18n/locales/fr/chat.json
index 993bb68ac..a353ff1a6 100644
--- a/src/i18n/locales/fr/chat.json
+++ b/src/i18n/locales/fr/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "Vous êtes en mode auto-hébergé. Les modèles cloud ne peuvent pas être utilisés ici — configurez votre propre modèle cloud local pour que tout continue de fonctionner.",
"you-are-using-self-hosted-mode-mcp": "Vous utilisez le mode auto-hébergé. Entrez les clés de recherche Google dans « MCP et Outils » pour garantir le bon fonctionnement d'Eigent.",
"palm-springs-tennis-trip-planner": "Planificateur de voyage tennis à Palm Springs",
- "palm-springs-tennis-trip-planner-message": "Nous sommes deux fans de tennis et voulons aller voir le tournoi de tennis à Palm Springs. J'habite à SF - veuillez préparer un itinéraire détaillé avec vols, hôtels, choses à faire pendant 3 jours - autour de la période des demi-finales/finales. Nous aimons la randonnée, la nourriture végétalienne et les spas. Notre budget est de 5 000 $. L'itinéraire doit être un calendrier détaillé de l'heure, de l'activité, du coût, d'autres détails et, le cas échéant, un lien pour acheter des billets/faire des réservations, etc. pour l'article. Certaines préférences 1. L'accès au spa serait agréable mais pas nécessaire 2. Lorsque vous aurez terminé cette tâche, veuillez générer un rapport HTML sur ce voyage.",
+ "palm-springs-tennis-trip-planner-message": "Je suis un fan de tennis et je souhaite voir le tournoi de tennis à Palm Springs. Je vis à San Francisco (SF). Veuillez préparer un itinéraire détaillé de trois jours avec les vols, les hôtels et les activités pour la période des demi-finales/finales. J'aime la randonnée, la nourriture végétalienne et les spas. Mon budget est de 5 000 $. L'itinéraire doit être un calendrier détaillé indiquant les heures, les activités, les coûts et d'autres détails, et — le cas échéant — des liens pour acheter des billets ou faire des réservations. Préférences : 1. L'accès à un spa serait appréciable mais n'est pas nécessaire. 2. Lorsque vous aurez terminé cette tâche, veuillez générer un rapport HTML sur le voyage.",
"bank-transfer-csv-analysis-and-visualization": "Analyse et visualisation CSV des virements bancaires",
"bank-transfer-csv-analysis-and-visualization-message": "Créez un fichier CSV de virements bancaires fictifs comprenant 10 colonnes et 10 lignes. Lisez le fichier CSV généré et résumez les données, générez un graphique pour visualiser les tendances ou les informations pertinentes à partir des données.",
"find-duplicate-files-in-downloads-folder": "Rechercher les fichiers en double dans le dossier Téléchargements",
diff --git a/src/i18n/locales/fr/setting.json b/src/i18n/locales/fr/setting.json
index 6081007ab..773cb7f61 100644
--- a/src/i18n/locales/fr/setting.json
+++ b/src/i18n/locales/fr/setting.json
@@ -20,7 +20,7 @@
"data-privacy-description": "Eigent is built on a local-first principle to ensure your privacy. Your data remains on your device by default. Cloud features are optional and only use the minimum data necessary to function. For full details, visit our",
"privacy-policy": "Privacy Policy",
"how-we-handle-your-data": "How we handle your data",
- "how-we-handle-your-data-line-1": "We only use the essential data needed to run your tasks:",
+ "how-we-handle-your-data-line-1": "We only use the essential data needed to run your tasks",
"how-we-handle-your-data-line-1-line-1": "Eigent may capture screenshots to analyze UI elements, read text, and determine the next action, just as you would.",
"how-we-handle-your-data-line-1-line-2": "Eigent may use your mouse and keyboard to access local software and files you specify.",
"how-we-handle-your-data-line-1-line-3": "Only the minimum task data is sent to AI model providers or the third-party integrations you enable; we have zero data-retention",
@@ -88,10 +88,10 @@
"invalid-json": "Invalid JSON",
"coming-soon": "Coming Soon",
"uninstall": "Uninstall",
- "install": "Install",
- "add-your-mcp-server": "Add your MCP server",
- "add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Add a local MCP server by providing a valid JSON configuration.",
- "learn-more": "Learn more",
+ "install": "Installer",
+ "add-your-agent": "Ajoutez votre Agent",
+ "add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Ajoutez un serveur MCP local en fournissant une configuration JSON valide.",
+ "learn-more": "En savoir plus",
"installing": "Installing...",
"edit-mcp-config": "Edit MCP Config",
"name": "Name",
diff --git a/src/i18n/locales/fr/workforce.json b/src/i18n/locales/fr/workforce.json
index a491d69ab..0c2cbf3cf 100644
--- a/src/i18n/locales/fr/workforce.json
+++ b/src/i18n/locales/fr/workforce.json
@@ -3,7 +3,7 @@
"new-worker": "Nouveau travailleur",
"edit": "Modifier",
"configure-mcp-server": "Configurer le serveur MCP",
- "add-your-mcp-server": "Ajouter votre serveur MCP",
+ "add-your-agent": "Ajoutez votre Agent",
"cancel": "Annuler",
"save-changes": "Enregistrer les modifications",
"description-optional": "Description (facultatif)",
diff --git a/src/i18n/locales/it/chat.json b/src/i18n/locales/it/chat.json
index 2bcf7878e..248dc0006 100644
--- a/src/i18n/locales/it/chat.json
+++ b/src/i18n/locales/it/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "Sei in modalità Self-hosted. I modelli cloud non possono essere utilizzati qui — configura il tuo modello cloud locale per mantenere tutto in funzione.",
"you-are-using-self-hosted-mode-mcp": "Stai utilizzando la modalità Self-hosted. Inserisci le Chiavi di Ricerca Google in \"MCP e Strumenti\" per garantire che Eigent funzioni correttamente.",
"palm-springs-tennis-trip-planner": "Pianificatore di Viaggio per il Torneo di Tennis di Palm Springs",
- "palm-springs-tennis-trip-planner-message": "Siamo due appassionati di tennis e vogliamo andare a vedere il torneo di tennis a Palm Springs. Vivo a SF - per favore prepara un itinerario dettagliato con voli, hotel, cose da fare per 3 giorni - intorno al periodo delle semifinali/finali. Ci piace fare escursioni, cibo vegano e spa. Il nostro budget è di 5.000$. L'itinerario dovrebbe essere una timeline dettagliata di orario, attività, costo, altri dettagli e, se applicabile, un link per acquistare biglietti/effettuare prenotazioni, ecc. per l'articolo. Alcune preferenze: 1. L'accesso alla spa sarebbe bello ma non necessario 2. Quando avrai finito questo compito, genera un report HTML su questo viaggio.",
+ "palm-springs-tennis-trip-planner-message": "Sono un appassionato di tennis e vorrei vedere il torneo di tennis a Palm Springs. Vivo a San Francisco (SF). Per favore, prepari un itinerario dettagliato di tre giorni con voli, hotel e attività per il periodo delle semifinali/finali. Mi piacciono le escursioni, il cibo vegano e le spa. Il mio budget è di 5.000 $. L'itinerario dovrebbe essere una cronologia dettagliata che mostri orari, attività, costi e altri dettagli e, ove applicabile, i link per acquistare i biglietti o effettuare prenotazioni. Preferenze: 1. L'accesso a una spa sarebbe gradito ma non è necessario. 2. Al termine di questo compito, la prego di generare un report in HTML sul viaggio.",
"bank-transfer-csv-analysis-and-visualization": "Analisi e Visualizzazione CSV di Bonifici Bancari",
"bank-transfer-csv-analysis-and-visualization-message": "Crea un file CSV di bonifici bancari fittizio con 10 colonne e 10 righe. Leggi il file CSV generato e riassumi i dati, genera un grafico per visualizzare tendenze o intuizioni pertinenti dai dati.",
"find-duplicate-files-in-downloads-folder": "Trova File Duplicati nella Cartella Download",
diff --git a/src/i18n/locales/it/setting.json b/src/i18n/locales/it/setting.json
index 482c7ec2d..8d69f2eea 100644
--- a/src/i18n/locales/it/setting.json
+++ b/src/i18n/locales/it/setting.json
@@ -20,7 +20,7 @@
"data-privacy-description": "Eigent è costruito su un principio local-first per garantire la tua privacy. I tuoi dati rimangono sul tuo dispositivo per impostazione predefinita. Le funzionalità cloud sono opzionali e utilizzano solo i dati minimi necessari per funzionare. Per tutti i dettagli, visita la nostra",
"privacy-policy": "Informativa sulla privacy",
"how-we-handle-your-data": "Come gestiamo i tuoi dati",
- "how-we-handle-your-data-line-1": "Utilizziamo solo i dati essenziali necessari per eseguire le tue attività:",
+ "how-we-handle-your-data-line-1": "Utilizziamo solo i dati essenziali necessari per eseguire le tue attività",
"how-we-handle-your-data-line-1-line-1": "Eigent può acquisire screenshot per analizzare elementi dell'interfaccia utente, leggere testo e determinare l'azione successiva, proprio come faresti tu.",
"how-we-handle-your-data-line-1-line-2": "Eigent può utilizzare il tuo mouse e la tua tastiera per accedere a software e file locali da te specificati.",
"how-we-handle-your-data-line-1-line-3": "Solo i dati minimi necessari per l'attività vengono inviati ai fornitori di modelli AI o alle integrazioni di terze parti da te abilitate; non abbiamo alcuna conservazione dei dati",
@@ -89,7 +89,7 @@
"coming-soon": "Prossimamente",
"uninstall": "Disinstalla",
"install": "Installa",
- "add-your-mcp-server": "Aggiungi il tuo server MCP",
+ "add-your-agent": "Aggiungi il tuo Agente",
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Aggiungi un server MCP locale fornendo una configurazione JSON valida.",
"learn-more": "Scopri di più",
"installing": "Installazione in corso...",
diff --git a/src/i18n/locales/it/workforce.json b/src/i18n/locales/it/workforce.json
index e490fcd91..518ff722d 100644
--- a/src/i18n/locales/it/workforce.json
+++ b/src/i18n/locales/it/workforce.json
@@ -3,7 +3,7 @@
"new-worker": "Nuovo Worker",
"edit": "Modifica",
"configure-mcp-server": "Configura server MCP",
- "add-your-mcp-server": "Aggiungi il tuo server MCP",
+ "add-your-agent": "Aggiungi il tuo Agente",
"cancel": "Annulla",
"save-changes": "Salva modifiche",
"description-optional": "Descrizione (Opzionale)",
diff --git a/src/i18n/locales/ja/chat.json b/src/i18n/locales/ja/chat.json
index ea6164bad..57f398da9 100644
--- a/src/i18n/locales/ja/chat.json
+++ b/src/i18n/locales/ja/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "セルフホストモードを使用しています。クラウドモデルはここでは使用できません。続行するには、独自のローカルクラウドモデルを設定してください。",
"you-are-using-self-hosted-mode-mcp": "セルフホストモードを使用しています。Eigentが正しく機能するように、「MCPとツール」にGoogle検索キーを入力してください。",
"palm-springs-tennis-trip-planner": "パームスプリングステニス旅行プランナー",
- "palm-springs-tennis-trip-planner-message": "私は2人のテニスファンで、パームスプリングスのテニストーナメントを見に行きたいです。SFに住んでいます。準決勝/決勝の頃の3日間の詳細な旅程(フライト、ホテル、アクティビティ込み)を作成してください。ハイキング、ビーガンフード、スパが好きです。予算は5,000ドルです。旅程には、時間、アクティビティ、費用、その他の詳細、および可能な場合はチケット購入/予約などのリンクを含む詳細なタイムラインを含めてください。いくつか希望があります。1.スパへのアクセスがあれば良いですが、必須ではありません。2.このタスクが完了したら、この旅行に関するHTMLレポートを生成してください。",
+ "palm-springs-tennis-trip-planner-message": "私はテニスファンで、パームスプリングスで開催されるテニストーナメントを見に行きたいです。私はサンフランシスコ(SF)に住んでいます。準決勝・決勝の期間に合わせた、航空券、ホテル、アクティビティを含む3日間の詳細な旅程を作成してください。ハイキング、ヴィーガンフード、スパが好きです。予算は5,000ドルです。旅程には、時間、アクティビティ、費用、その他の詳細を示す詳細なタイムラインを含め、該当する場合にはチケットの購入や予約を行うためのリンクも記載してください。希望:1. スパの利用は必須ではありませんが、できれば嬉しいです。2. このタスクが完了したら、旅行に関するHTMLレポートを生成してください。",
"bank-transfer-csv-analysis-and-visualization": "銀行振込CSV分析と可視化",
"bank-transfer-csv-analysis-and-visualization-message": "10列10行のモック銀行振込CSVファイルを作成してください。生成されたCSVファイルを読み込み、データを要約し、データから関連する傾向や洞察を可視化するためのグラフを生成してください。",
"find-duplicate-files-in-downloads-folder": "ダウンロードフォルダ内の重複ファイルを検索",
diff --git a/src/i18n/locales/ja/setting.json b/src/i18n/locales/ja/setting.json
index b74f03917..956e5ca3d 100644
--- a/src/i18n/locales/ja/setting.json
+++ b/src/i18n/locales/ja/setting.json
@@ -20,7 +20,7 @@
"data-privacy-description": "Eigentはプライバシーを確保するためにローカルファーストの原則に基づいて構築されています。デフォルトでは、データはお客様のデバイスに残ります。クラウド機能はオプションであり、機能するために必要な最小限のデータのみを使用します。詳細については、当社の",
"privacy-policy": "プライバシーポリシー",
"how-we-handle-your-data": "データの取り扱い方法",
- "how-we-handle-your-data-line-1": "タスクを実行するために必要な最小限のデータのみを使用します。",
+ "how-we-handle-your-data-line-1": "タスクを実行するために必要な最小限のデータのみを使用します",
"how-we-handle-your-data-line-1-line-1": "Eigentは、UI要素を分析し、テキストを読み取り、次のアクションを決定するために、お客様が行うのと同じようにスクリーンショットをキャプチャする場合があります。",
"how-we-handle-your-data-line-1-line-2": "Eigentは、指定したローカルソフトウェアおよびファイルにアクセスするために、マウスとキーボードを使用する場合があります。",
"how-we-handle-your-data-line-1-line-3": "有効にしたAIモデルプロバイダーまたはサードパーティ統合に送信されるのは、タスクの最小限のデータのみです。データ保持はありません。",
@@ -89,7 +89,7 @@
"coming-soon": "近日公開",
"uninstall": "アンインストール",
"install": "インストール",
- "add-your-mcp-server": "MCPサーバーを追加",
+ "add-your-agent": "エージェントを追加",
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "有効なJSON構成を提供して、ローカルMCPサーバーを追加します。",
"learn-more": "詳細はこちら",
"installing": "インストール中...",
diff --git a/src/i18n/locales/ja/workforce.json b/src/i18n/locales/ja/workforce.json
index 88e251788..d205ea1a6 100644
--- a/src/i18n/locales/ja/workforce.json
+++ b/src/i18n/locales/ja/workforce.json
@@ -3,7 +3,7 @@
"new-worker": "新規ワーカー",
"edit": "編集",
"configure-mcp-server": "MCPサーバーの設定",
- "add-your-mcp-server": "MCPサーバーを追加",
+ "add-your-agent": "エージェントを追加",
"cancel": "キャンセル",
"save-changes": "変更を保存",
"description-optional": "説明(任意)",
diff --git a/src/i18n/locales/ko/chat.json b/src/i18n/locales/ko/chat.json
index a115e5c2e..d71509397 100644
--- a/src/i18n/locales/ko/chat.json
+++ b/src/i18n/locales/ko/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "셀프 호스팅 모드를 사용 중입니다. 클라우드 모델은 여기서 사용할 수 없습니다. 계속 진행하려면 자체 로컬 클라우드 모델을 설정하세요.",
"you-are-using-self-hosted-mode-mcp": "셀프 호스팅 모드를 사용 중입니다. Eigent가 제대로 작동하도록 \"MCP 및 도구\"에 Google 검색 키를 입력하세요.",
"palm-springs-tennis-trip-planner": "팜스프링스 테니스 여행 플래너",
- "palm-springs-tennis-trip-planner-message": "저는 두 명의 테니스 팬이며 팜스프링스에서 테니스 토너먼트를 보러 가고 싶습니다. 저는 샌프란시스코에 거주합니다. 준결승/결승이 열리는 시기 주변의 3일간의 항공편, 호텔, 할 일에 대한 자세한 일정 계획을 세워주세요. 하이킹, 비건 음식, 스파를 좋아합니다. 저희 예산은 5,000달러입니다. 일정에는 시간, 활동, 비용, 기타 세부 정보 및 가능한 경우 티켓 구매/예약 등을 위한 링크가 포함된 자세한 타임라인이 있어야 합니다. 몇 가지 선호 사항: 1. 스파 이용이 가능하면 좋지만 필수는 아닙니다. 2. 이 작업을 마치면 이 여행에 대한 HTML 보고서를 생성해 주세요.",
+ "palm-springs-tennis-trip-planner-message": "저는 테니스 팬이며 팜스프링스에서 열리는 테니스 토너먼트를 보고 싶습니다. 저는 샌프란시스코(SF)에 살고 있습니다. 준결승/결승 기간에 맞춘 항공편, 호텔, 활동이 포함된 상세한 3일 여행 일정을 준비해 주세요. 저는 하이킹, 비건 음식, 스파를 좋아합니다. 제 예산은 5,000달러입니다. 일정에는 시간, 활동, 비용 및 기타 세부 정보가 포함된 상세한 타임라인이 있어야 하며, 해당하는 경우 티켓 구매 또는 예약을 위한 링크도 포함되어야 합니다. 선호 사항: 1. 스파 이용은 필수 사항은 아니지만 가능하면 좋겠습니다. 2. 이 작업을 마치면 여행에 대한 HTML 보고서를 생성해 주세요.",
"bank-transfer-csv-analysis-and-visualization": "은행 송금 CSV 분석 및 시각화",
"bank-transfer-csv-analysis-and-visualization-message": "10개의 열과 10개의 행으로 구성된 모의 은행 송금 CSV 파일을 생성하세요. 생성된 CSV 파일을 읽고 데이터를 요약하고 데이터에서 관련 추세 또는 통찰력을 시각화할 차트를 생성하세요.",
"find-duplicate-files-in-downloads-folder": "다운로드 폴더에서 중복 파일 찾기",
diff --git a/src/i18n/locales/ko/setting.json b/src/i18n/locales/ko/setting.json
index 8c825878c..b98d3520b 100644
--- a/src/i18n/locales/ko/setting.json
+++ b/src/i18n/locales/ko/setting.json
@@ -20,7 +20,7 @@
"data-privacy-description": "Eigent는 개인정보 보호를 위해 로컬 우선 원칙을 기반으로 구축되었습니다. 기본적으로 데이터는 사용자의 기기에 남아 있습니다. 클라우드 기능은 선택 사항이며 작동에 필요한 최소한의 데이터만 사용합니다. 자세한 내용은 당사의",
"privacy-policy": "개인정보 처리방침",
"how-we-handle-your-data": "데이터 처리 방식",
- "how-we-handle-your-data-line-1": "작업 실행에 필요한 필수 데이터만 사용합니다:",
+ "how-we-handle-your-data-line-1": "작업 실행에 필요한 필수 데이터만 사용합니다",
"how-we-handle-your-data-line-1-line-1": "Eigent는 UI 요소를 분석하고 텍스트를 읽고 다음 작업을 결정하기 위해 스크린샷을 캡처할 수 있습니다. 사용자가 하는 방식과 같습니다.",
"how-we-handle-your-data-line-1-line-2": "Eigent는 사용자가 지정한 로컬 소프트웨어 및 파일에 액세스하기 위해 마우스와 키보드를 사용할 수 있습니다.",
"how-we-handle-your-data-line-1-line-3": "AI 모델 제공업체 또는 사용자가 활성화한 타사 통합에 전송되는 데이터는 최소한의 작업 데이터뿐이며, 데이터 보존은 전혀 없습니다.",
@@ -89,7 +89,7 @@
"coming-soon": "출시 예정",
"uninstall": "제거",
"install": "설치",
- "add-your-mcp-server": "MCP 서버 추가",
+ "add-your-agent": "에이전트 추가",
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "유효한 JSON 구성을 제공하여 로컬 MCP 서버를 추가하세요.",
"learn-more": "더 알아보기",
"installing": "설치 중...",
diff --git a/src/i18n/locales/ko/workforce.json b/src/i18n/locales/ko/workforce.json
index e8495536e..467441bbf 100644
--- a/src/i18n/locales/ko/workforce.json
+++ b/src/i18n/locales/ko/workforce.json
@@ -3,7 +3,7 @@
"new-worker": "새 작업자",
"edit": "편집",
"configure-mcp-server": "MCP 서버 구성",
- "add-your-mcp-server": "MCP 서버 추가",
+ "add-your-agent": "에이전트 추가",
"cancel": "취소",
"save-changes": "변경 사항 저장",
"description-optional": "설명 (선택 사항)",
diff --git a/src/i18n/locales/ru/chat.json b/src/i18n/locales/ru/chat.json
index 0418a60cc..6c685dbf4 100644
--- a/src/i18n/locales/ru/chat.json
+++ b/src/i18n/locales/ru/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "Вы используете режим самостоятельного хостинга. Облачные модели здесь использовать нельзя — настройте собственную локальную облачную модель, чтобы все работало.",
"you-are-using-self-hosted-mode-mcp": "Вы используете режим самостоятельного хостинга. Введите ключи Google Search в разделе «MCP и инструменты», чтобы Eigent работал должным образом.",
"palm-springs-tennis-trip-planner": "Планировщик поездки на теннисный турнир в Палм-Спрингс",
- "palm-springs-tennis-trip-planner-message": "Мы два теннисных фаната и хотим поехать на теннисный турнир в Палм-Спрингс. Мы живем в Сан-Франциско — пожалуйста, составьте подробный маршрут с указанием авиабилетов, отелей, развлечений на 3 дня — примерно во время полуфинала/финала. Нам нравится хайкинг, веганская еда и спа. Наш бюджет — 5000 долларов. Маршрут должен быть подробным графиком времени, мероприятий, стоимости, других деталей и, если применимо, ссылкой для покупки билетов/бронирования и т.д. для каждого пункта. Некоторые предпочтения: 1. Доступ к спа был бы желателен, но не обязателен. 2. По завершении этой задачи, пожалуйста, сгенерируйте HTML-отчет об этой поездке.",
+ "palm-springs-tennis-trip-planner-message": "Я фанат тенниса и хочу посмотреть теннисный турнир в Палм-Спрингс. Я живу в Сан-Франциско (SF). Пожалуйста, подготовьте подробный трехдневный маршрут с перелетами, отелями и мероприятиями на период полуфиналов/финалов. Мне нравятся пешие прогулки, веганская еда и спа. Мой бюджет составляет 5000 долларов. Маршрут должен представлять собой подробный график с указанием времени, мероприятий, затрат и других деталей, а также, где это применимо, ссылок для покупки билетов или бронирования. Предпочтения: 1. Доступ в спа был бы неплох, но не обязателен. 2. Когда вы закончите эту задачу, пожалуйста, сгенерируйте HTML-отчет о поездке.",
"bank-transfer-csv-analysis-and-visualization": "Анализ и визуализация CSV-файлов банковских переводов",
"bank-transfer-csv-analysis-and-visualization-message": "Создайте пример CSV-файла банковских переводов, включающий 10 столбцов и 10 строк. Прочитайте сгенерированный CSV-файл и обобщите данные, сгенерируйте диаграмму для визуализации соответствующих тенденций или выводов из данных.",
"find-duplicate-files-in-downloads-folder": "Поиск дубликатов файлов в папке загрузок",
diff --git a/src/i18n/locales/ru/setting.json b/src/i18n/locales/ru/setting.json
index 9cfb0d0b8..28bd3599c 100644
--- a/src/i18n/locales/ru/setting.json
+++ b/src/i18n/locales/ru/setting.json
@@ -20,7 +20,7 @@
"data-privacy-description": "Eigent построен по принципу \"сначала локально\", чтобы обеспечить вашу конфиденциальность. Ваши данные по умолчанию остаются на вашем устройстве. Облачные функции являются необязательными и используют только минимальные необходимые данные для функционирования. Полные сведения см. в нашей",
"privacy-policy": "Политике конфиденциальности",
"how-we-handle-your-data": "Как мы обрабатываем ваши данные",
- "how-we-handle-your-data-line-1": "Мы используем только необходимые данные для выполнения ваших задач:",
+ "how-we-handle-your-data-line-1": "Мы используем только необходимые данные для выполнения ваших задач",
"how-we-handle-your-data-line-1-line-1": "Eigent может делать снимки экрана для анализа элементов пользовательского интерфейса, чтения текста и определения следующего действия, точно так же, как это сделали бы вы.",
"how-we-handle-your-data-line-1-line-2": "Eigent может использовать вашу мышь и клавиатуру для доступа к локальному программному обеспечению и файлам, которые вы указываете.",
"how-we-handle-your-data-line-1-line-3": "Только минимальные данные задачи отправляются поставщикам моделей ИИ или сторонним интеграциям, которые вы включаете; мы не храним данные.",
@@ -89,7 +89,7 @@
"coming-soon": "Скоро",
"uninstall": "Удалить",
"install": "Установить",
- "add-your-mcp-server": "Добавьте свой MCP-сервер",
+ "add-your-agent": "Добавьте своего агента",
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Добавьте локальный MCP-сервер, предоставив допустимую JSON-конфигурацию.",
"learn-more": "Подробнее",
"installing": "Установка...",
diff --git a/src/i18n/locales/ru/workforce.json b/src/i18n/locales/ru/workforce.json
index 79bfa5269..0087b697d 100644
--- a/src/i18n/locales/ru/workforce.json
+++ b/src/i18n/locales/ru/workforce.json
@@ -3,7 +3,7 @@
"new-worker": "Новый сотрудник",
"edit": "Редактировать",
"configure-mcp-server": "Настроить MCP-сервер",
- "add-your-mcp-server": "Добавить ваш MCP-сервер",
+ "add-your-agent": "Добавьте своего агента",
"cancel": "Отмена",
"save-changes": "Сохранить изменения",
"description-optional": "Описание (необязательно)",
diff --git a/src/i18n/locales/zh-Hans/chat.json b/src/i18n/locales/zh-Hans/chat.json
index 311d81ceb..c391a1d96 100644
--- a/src/i18n/locales/zh-Hans/chat.json
+++ b/src/i18n/locales/zh-Hans/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "您正在使用自部署模式。云端模型不能在这里使用 — 设置您自己的本地云模型以保持运行。",
"you-are-using-self-hosted-mode-mcp": "您正在使用自部署模式。在“MCP和工具”中输入 Google 搜索密钥以确保 Eigent 正常工作。",
"palm-springs-tennis-trip-planner": "棕榈泉网球之旅规划师",
- "palm-springs-tennis-trip-planner-message": "我是两个网球爱好者,想去观看2026年棕榈泉的网球比赛。我住在旧金山——请准备一个详细的行程,包括航班、酒店、为期3天的活动安排——围绕半决赛/决赛的时间。我们喜欢徒步、素食和 SPA。预算为5,000美元。行程应是一个详细的时间表,包括时间、活动、费用、其他细节,以及购买门票/预订的链接(如适用)。完成任务后,请生成一份关于此次旅行的 HTML 报告;编写此计划的摘要,并将文本摘要和报告 HTML 链接发送到 Slack #tennis-trip-sf 频道。",
+ "palm-springs-tennis-trip-planner-message": "我是一名网球迷,想去棕榈泉看网球锦标赛。我住在旧金山(SF)。请为我准备一份为期三天的详细行程,包括半决赛/决赛期间的航班、酒店和活动。我喜欢徒步、素食和水疗。我的预算是5000美元。行程应为详细的时间表,显示时间、活动、费用和其他细节,并在适用的情况下提供购买门票或进行预订的链接。偏好:1. 如果能有水疗中心就更好了,但不是必须的。2. 完成此任务后,请生成一份关于此次旅行的HTML报告。",
"bank-transfer-csv-analysis-and-visualization": "银行转账 CSV 分析和可视化",
"bank-transfer-csv-analysis-and-visualization-message": "创建一个包含10列和10行的模拟银行转账 CSV 文件。读取生成的 CSV 文件并总结数据,生成一个图表来可视化相关趋势或洞察数据。",
"find-duplicate-files-in-downloads-folder": "查找下载文件夹中的重复文件",
diff --git a/src/i18n/locales/zh-Hans/setting.json b/src/i18n/locales/zh-Hans/setting.json
index ceb636ede..ca3dbe49e 100644
--- a/src/i18n/locales/zh-Hans/setting.json
+++ b/src/i18n/locales/zh-Hans/setting.json
@@ -20,8 +20,8 @@
"data-privacy-description": "Eigent 基于本地优先原则确保您的隐私。您的数据默认存储在您的设备上。云功能是可选的,仅使用最小的数据来实现功能。详细信息请访问我们的",
"privacy-policy": "隐私政策",
"how-we-handle-your-data": "我们如何处理您的数据",
- "we-only-use-the-essential-data-needed-to-run-your-tasks": "我们仅使用运行您的任务所需的关键数据:",
- "how-we-handle-your-data-line-1": "我们仅使用运行您的任务所需的关键数据:",
+ "we-only-use-the-essential-data-needed-to-run-your-tasks": "我们仅使用运行您的任务所需的关键数据",
+ "how-we-handle-your-data-line-1": "我们仅使用运行您的任务所需的关键数据",
"how-we-handle-your-data-line-1-line-1": "Eigent 可能捕获您的屏幕截图以分析 UI 元素、读取文本并确定下一步操作,就像您一样。",
"how-we-handle-your-data-line-1-line-2": "Eigent 可能使用您的鼠标和键盘访问您指定的本地软件和文件。",
"how-we-handle-your-data-line-1-line-3": "只有最少的任务数据被发送到 AI 模型提供商或您启用的第三方集成;我们与这些提供商没有数据保留协议",
@@ -89,7 +89,7 @@
"coming-soon": "即将推出",
"uninstall": "卸载",
"install": "安装",
- "add-your-mcp-server": "添加您的 MCP 服务器",
+ "add-your-agent": "添加您的智能体",
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "通过提供有效的 JSON 配置添加本地 MCP 服务器。",
"learn-more": "了解更多",
"installing": "安装中...",
diff --git a/src/i18n/locales/zh-Hans/workforce.json b/src/i18n/locales/zh-Hans/workforce.json
index 02a54577e..6df1fdd62 100644
--- a/src/i18n/locales/zh-Hans/workforce.json
+++ b/src/i18n/locales/zh-Hans/workforce.json
@@ -3,7 +3,7 @@
"new-worker": "增加 Worker",
"edit": "编辑",
"configure-mcp-server": "配置 MCP 服务器",
- "add-your-mcp-server": "添加你的 MCP 服务器",
+ "add-your-agent": "添加您的智能体",
"cancel": "取消",
"save-changes": "保存更改",
"description-optional": "描述(可选)",
diff --git a/src/i18n/locales/zh-Hant/chat.json b/src/i18n/locales/zh-Hant/chat.json
index d157c2f0d..e343dc3a6 100644
--- a/src/i18n/locales/zh-Hant/chat.json
+++ b/src/i18n/locales/zh-Hant/chat.json
@@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "您正在使用自部署模式。雲端模型不能在這裡使用 — 設置您自己的本地雲模型以保持運行。",
"you-are-using-self-hosted-mode-mcp": "您正在使用自部署模式。在“MCP和工具”中輸入 Google 搜索密鑰以確保 Eigent 正常工作。",
"palm-springs-tennis-trip-planner": "棕榈泉網球之旅規劃師",
- "palm-springs-tennis-trip-planner-message": "我是两个网球爱好者,想去观看2026年棕榈泉的网球比赛。我住在旧金山——请准备一个详细的行程,包括航班、酒店、为期3天的活动安排——围绕半决赛/决赛的时间。我们喜欢徒步、素食和 SPA。预算为5,000美元。行程应是一个详细的时间表,包括时间、活动、费用、其他细节,以及购买门票/预订的链接(如适用)。完成任务后,请生成一份关于此次旅行的 HTML 报告;編寫此計劃的摘要,並將文本摘要和报告 HTML 鏈接發送到 Slack #tennis-trip-sf 頻道。",
+ "palm-springs-tennis-trip-planner-message": "我是一名網球迷,想去棕櫚泉看網球錦標賽。我住在舊金山(SF)。請為我準備一份為期三天的詳細行程,包括半決賽/決賽期間的航班、酒店和活動。我喜歡徒步、素食和水療。我的預算是5000美元。行程應為詳細的時間表,顯示時間、活動、費用和其他細節,並在適用的情況下提供購買門票或進行預訂的連結。偏好:1. 如果能有水療中心就更好了,但不是必須的。2. 完成此任務後,請生成一份關於此次旅行的HTML報告。",
"bank-transfer-csv-analysis-and-visualization": "銀行轉賬 CSV 分析和視覺化",
"bank-transfer-csv-analysis-and-visualization-message": "創建一個包含10列和10行的模擬銀行轉賬 CSV 文件。讀取生成的 CSV 文件並總結數據,生成一個圖表來視覺化相關趨勢或洞察數據。",
"find-duplicate-files-in-downloads-folder": "查找下载文件夹中的重复文件",
diff --git a/src/i18n/locales/zh-Hant/setting.json b/src/i18n/locales/zh-Hant/setting.json
index 3f501b434..f0891eae1 100644
--- a/src/i18n/locales/zh-Hant/setting.json
+++ b/src/i18n/locales/zh-Hant/setting.json
@@ -20,8 +20,8 @@
"data-privacy-description": "Eigent 以本地優先原則確保您的隱私。您的數據預設儲存在您的設備上。雲端功能是可選的,僅使用最少量的數據來實現功能。詳細資訊請參閱我們的",
"privacy-policy": "隱私政策",
"how-we-handle-your-data": "我們如何處理您的數據",
- "we-only-use-the-essential-data-needed-to-run-your-tasks": "我們僅使用運行您的任務所需的關鍵數據:",
- "how-we-handle-your-data-line-1": "我們僅使用運行您的任務所需的關鍵數據:",
+ "we-only-use-the-essential-data-needed-to-run-your-tasks": "我們僅使用運行您的任務所需的關鍵數據",
+ "how-we-handle-your-data-line-1": "我們僅使用運行您的任務所需的關鍵數據",
"how-we-handle-your-data-line-1-line-1": "Eigent 可能會擷取您的螢幕截圖以分析 UI 元素、讀取文字並判斷下一步操作,就像您一樣。",
"how-we-handle-your-data-line-1-line-2": "Eigent 可能會使用您的滑鼠和鍵盤存取您指定的本地軟體和檔案。",
"how-we-handle-your-data-line-1-line-3": "僅有最少量的任務數據會傳送至 AI 模型供應商或您啟用的第三方整合;我們與這些供應商沒有數據保留協議。",
@@ -89,7 +89,7 @@
"coming-soon": "即將推出",
"uninstall": "解除安裝",
"install": "安裝",
- "add-your-mcp-server": "新增您的 MCP 伺服器",
+ "add-your-agent": "新增您的智能體",
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "透過提供有效的 JSON 設定新增本地 MCP 伺服器。",
"learn-more": "了解更多",
"installing": "安裝中...",
diff --git a/src/i18n/locales/zh-Hant/workforce.json b/src/i18n/locales/zh-Hant/workforce.json
index 6921339bf..5060efaa3 100644
--- a/src/i18n/locales/zh-Hant/workforce.json
+++ b/src/i18n/locales/zh-Hant/workforce.json
@@ -3,7 +3,7 @@
"new-worker": "新增 Worker",
"edit": "編輯",
"configure-mcp-server": "設定 MCP 伺服器",
- "add-your-mcp-server": "新增您的 MCP 伺服器",
+ "add-your-agent": "新增您的智能體",
"cancel": "取消",
"save-changes": "儲存變更",
"description-optional": "描述(可選)",
diff --git a/src/pages/History.tsx b/src/pages/History.tsx
index 874c532e6..c0e8548e7 100644
--- a/src/pages/History.tsx
+++ b/src/pages/History.tsx
@@ -295,7 +295,7 @@ export default function Home() {
}}
className={`${
chatStore.activeTaskId === taskId ? "!bg-white-100%" : ""
- } relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center gap-md flex-1 w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] px-6 shadow-history-item`}
+ } relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center gap-md flex-initial w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] px-6 shadow-history-item`}
>
@@ -555,7 +555,7 @@ export default function Home() {
chatStore.activeTaskId === task.task_id
? "!bg-white-100%"
: ""
- } relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center flex-wrap gap-md flex-1 w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] p-6 shadow-history-item border border-solid border-border-disabled`}
+ } relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center flex-wrap gap-md flex-initial w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] p-6 shadow-history-item border border-solid border-border-disabled`}
>
{
- window.location.href = `https://www.eigent.ai/dashboard`;
+ window.location.href = `https://www.eigent.ai/dashboard?email=${authStore.email}`;
}}
variant="primary"
size="xs"
@@ -216,4 +216,4 @@ export default function SettingGeneral() {
);
-}
+}
\ No newline at end of file
diff --git a/src/pages/Setting/MCP.tsx b/src/pages/Setting/MCP.tsx
index 1d292c482..4f7cee6b6 100644
--- a/src/pages/Setting/MCP.tsx
+++ b/src/pages/Setting/MCP.tsx
@@ -61,6 +61,7 @@ export default function SettingMCP() {
// add: integrations list
const [integrations, setIntegrations] = useState([]);
+ const [refreshKey, setRefreshKey] = useState(0);
const [essentialIntegrations, setEssentialIntegrations] = useState([
{
key: "Search",
@@ -111,8 +112,57 @@ export default function SettingMCP() {
const list = Object.entries(res).map(([key, value]: [string, any]) => {
let onInstall = null;
- onInstall = () =>
- (window.location.href = `${baseURL}/api/oauth/${key.toLowerCase()}/login`);
+ // Special handling for Notion MCP
+ if (key.toLowerCase() === 'notion') {
+ onInstall = async () => {
+ try {
+ const response = await fetchPost("/install/tool/notion");
+ if (response.success) {
+ toast.success("Notion MCP installed successfully");
+ // Save to config to mark as installed
+ await proxyFetchPost("/api/configs", {
+ config_group: "Notion",
+ config_name: "MCP_REMOTE_CONFIG_DIR",
+ config_value: response.toolkit_name || "NotionMCPToolkit",
+ });
+ // Refresh the integrations list to show the installed state
+ fetchList();
+ // Force refresh IntegrationList component
+ setRefreshKey(prev => prev + 1);
+ } else {
+ toast.error(response.error || "Failed to install Notion MCP");
+ }
+ } catch (error: any) {
+ toast.error(error.message || "Failed to install Notion MCP");
+ }
+ };
+ } else if (key.toLowerCase() === 'google calendar') {
+ onInstall = async () => {
+ try {
+ const response = await fetchPost("/install/tool/google_calendar");
+ if (response.success) {
+ toast.success("Google Calendar installed successfully");
+ // Save to config to mark as installed
+ await proxyFetchPost("/api/configs", {
+ config_group: "Google Calendar",
+ config_name: "GOOGLE_CLIENT_ID",
+ config_value: response.toolkit_name || "GoogleCalendarToolkit",
+ });
+ // Refresh the integrations list to show the installed state
+ fetchList();
+ // Force refresh IntegrationList component
+ setRefreshKey(prev => prev + 1);
+ } else {
+ toast.error(response.error || "Failed to install Google Calendar");
+ }
+ } catch (error: any) {
+ toast.error(error.message || "Failed to install Google Calendar");
+ }
+ };
+ } else {
+ onInstall = () =>
+ (window.location.href = `${baseURL}/api/oauth/${key.toLowerCase()}/login`);
+ }
return {
key,
@@ -123,11 +173,17 @@ export default function SettingMCP() {
? `${t("setting.environmental-variables-required")}: ${value.env_vars.join(
", "
)}`
+ : key.toLowerCase() === 'notion'
+ ? "Notion workspace integration for reading and managing Notion pages"
+ : key.toLowerCase() === 'google calendar'
+ ? "Google Calendar integration for managing events and schedules"
: "",
onInstall,
};
});
- console.log("list", list);
+ console.log("API response:", res);
+ console.log("Generated list:", list);
+ console.log("Essential integrations:", essentialIntegrations);
setIntegrations(
list.filter(
@@ -351,7 +407,7 @@ export default function SettingMCP() {
MCP
-
+
diff --git a/src/pages/Setting/MCPMarket.tsx b/src/pages/Setting/MCPMarket.tsx
index 3a30476b7..7a6a3c698 100644
--- a/src/pages/Setting/MCPMarket.tsx
+++ b/src/pages/Setting/MCPMarket.tsx
@@ -10,6 +10,7 @@ import {
import {
Tooltip,
TooltipContent,
+ TooltipSimple,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { MCPEnvDialog } from "./components/MCPEnvDialog";
@@ -370,14 +371,9 @@ export default function MCPMarket({ onBack }: { onBack?: () => void }) {
{item.name}
-
-
-
-
-
- {item.description}
-
-
+
+
+
- {res.message}
-
- {
- navigator.clipboard.writeText(res.message);
- toast.success(t("setting.copied-to-clipboard"));
- }}
- >
- {t("setting.copy")}
-
-
-
- ),
- closeButton: true,
- });
+ const toastId = toast(t("setting.validate-failed"), {
+ description: res.message,
+ action: {
+ label: t("setting.close"),
+ onClick: () => {
+ toast.dismiss(toastId);
+ },
+ },
+ })
return;
}
@@ -370,36 +355,21 @@ export default function SettingModels() {
if (res.is_tool_calls && res.is_valid) {
console.log("success");
toast(t("setting.validate-success"), {
- description:
- t("setting.the-model-has-been-verified-to-support-function-calling-which-is-required-to-use-eigent"),
+ description: t(
+ "setting.the-model-has-been-verified-to-support-function-calling-which-is-required-to-use-eigent"
+ ),
closeButton: true,
});
} else {
console.log("failed", res.message);
- toast(t("setting.validate-failed"), {
- description: (
-
- {res.message}
-
- {
- navigator.clipboard.writeText(res.message);
- toast.success(t("setting.copied-to-clipboard"));
- }}
- >
- {t("setting.copy")}
-
-
-
- ),
- closeButton: true,
+ const toastId = toast(t("setting.validate-failed"), {
+ description: res.message,
+ action: {
+ label: t("setting.close"),
+ onClick: () => {
+ toast.dismiss(toastId);
+ },
+ },
});
return;
@@ -474,7 +444,9 @@ export default function SettingModels() {
if (!hasSearchKey) {
// Show warning toast instead of blocking
toast(t("setting.warning-google-search-not-configured"), {
- description: t("setting.search-functionality-may-be-limited-without-google-api"),
+ description: t(
+ "setting.search-functionality-may-be-limited-without-google-api"
+ ),
closeButton: true,
});
}
@@ -501,7 +473,9 @@ export default function SettingModels() {
if (!hasSearchKey) {
// Show warning toast instead of blocking
toast(t("setting.warning-google-search-not-configured"), {
- description: t("setting.search-functionality-may-be-limited-without-google-api"),
+ description: t(
+ "setting.search-functionality-may-be-limited-without-google-api"
+ ),
closeButton: true,
});
}
@@ -646,10 +620,8 @@ export default function SettingModels() {
? t("setting.gpt-4.1-mini")
: cloud_model_type === "gpt-4.1"
? t("setting.gpt-4.1")
- : cloud_model_type === "claude-opus-4-1-20250805"
- ? t("setting.claude-opus-4.1")
- : cloud_model_type === "claude-sonnet-4-20250514"
- ? t("setting.claude-sonnet-4")
+ : cloud_model_type === "claude-sonnet-4-5"
+ ? t("setting.claude-sonnet-4-5")
: cloud_model_type === "claude-3-5-haiku-20241022"
? t("setting.claude-3.5-haiku")
: cloud_model_type === "gpt-5"
@@ -683,8 +655,8 @@ export default function SettingModels() {
GPT-5
GPT-5 mini
GPT-5 nano
-
- Claude Sonnet 4
+
+ Claude Sonnet 4-5
Claude 3.5 Haiku
@@ -760,7 +732,9 @@ export default function SettingModels() {
{
@@ -805,7 +779,9 @@ export default function SettingModels() {
{
@@ -833,7 +809,9 @@ export default function SettingModels() {
{
diff --git a/src/pages/Setting/components/IntegrationList.tsx b/src/pages/Setting/components/IntegrationList.tsx
index a2a98b925..20fb328ad 100644
--- a/src/pages/Setting/components/IntegrationList.tsx
+++ b/src/pages/Setting/components/IntegrationList.tsx
@@ -23,7 +23,7 @@ interface IntegrationItem {
name: string;
desc: string | React.ReactNode;
env_vars: string[];
- onInstall: () => void;
+ onInstall: () => void | Promise;
}
@@ -312,7 +312,7 @@ export default function IntegrationList({
onConnect={onConnect}
activeMcp={activeMcp}
>
- {items.filter((item) => item.name !== "Notion").map((item) => {
+ {items.map((item) => {
const isInstalled = !!installed[item.key];
return (
- {t("setting.add-your-mcp-server")}
+ {t("setting.add-your-agent")}
diff --git a/src/pages/Setting/components/MCPEnvDialog.tsx b/src/pages/Setting/components/MCPEnvDialog.tsx
index 64951a1eb..b4a3bf9c7 100644
--- a/src/pages/Setting/components/MCPEnvDialog.tsx
+++ b/src/pages/Setting/components/MCPEnvDialog.tsx
@@ -12,19 +12,73 @@ import githubIcon from "@/assets/github.svg";
import { Input } from "@/components/ui/input";
import { useState, FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
+
interface EnvValue {
value: string;
required: boolean;
tip: string;
+ error?: string;
}
interface MCPEnvDialogProps {
showEnvConfig: boolean;
onClose: () => void;
- onConnect: (mcp:any) => void;
+ onConnect: (mcp: any) => void;
activeMcp?: any;
}
+export async function google_check(apiKey: string, searchEngineId: string) {
+ const query = "hello"; // rand word
+ const url = `https://www.googleapis.com/customsearch/v1?key=${apiKey}&cx=${searchEngineId}&q=${encodeURIComponent(
+ query
+ )}&num=1`;
+
+ try {
+ const res = await fetch(url);
+ if (!res.ok) {
+ throw new Error(`Google API error: ${res.status}`);
+ }
+ const data = await res.json();
+
+ if ("items" in data) {
+ return { success: true, message: "Google key is valid ✅", sample: data.items[0] };
+ } else {
+ return { success: false, message: "Google key invalid ❌", error: data.error };
+ }
+ } catch (err: any) {
+ return { success: false, message: `Google check failed: ${err.message}` };
+ }
+}
+
+export async function exa_check(apiKey: string) {
+ const query = "hello"; // rand search word
+
+ try {
+ const res = await fetch("https://api.exa.ai/search", {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ query }),
+ });
+ if (!res.ok) {
+ const data = await res.json();
+ throw new Error(`Exa API error: ${res.status} ${data.error}`);
+ }
+
+ const data = await res.json();
+ if ("results" in data) {
+ return { success: true, message: "Exa key is valid ✅", sample: data.results[0] };
+ } else {
+ return { success: false, message: "Exa key invalid ❌", error: data };
+ }
+ } catch (err: any) {
+ return { success: false, message: `Exa check failed: ${err.message}` };
+ }
+}
+
+
export const MCPEnvDialog: FC = ({
showEnvConfig,
onClose,
@@ -33,6 +87,7 @@ export const MCPEnvDialog: FC = ({
}) => {
const [envValues, setEnvValues] = useState<{ [key: string]: EnvValue }>({});
const [showKeys, setShowKeys] = useState<{ [key: string]: boolean }>({});
+ const [isValidating, setIsValidating] = useState(false);
const { t } = useTranslation();
useEffect(() => {
initializeEnvValues(activeMcp);
@@ -42,7 +97,7 @@ export const MCPEnvDialog: FC = ({
if (mcp?.install_command?.env) {
const initialValues: { [key: string]: EnvValue } = {};
Object.keys(mcp.install_command.env).forEach((key) => {
-
+
initialValues[key] = {
value: "",
required: true,
@@ -51,11 +106,11 @@ export const MCPEnvDialog: FC = ({
?.replace(/{{/g, "")
?.replace(/}}/g, "") || "",
};
- if(key==='EXA_API_KEY'){
- initialValues[key].required=false;
+ if (key === 'EXA_API_KEY') {
+ initialValues[key].required = false;
}
- if(key==='GOOGLE_REFRESH_TOKEN'){
- initialValues[key].required=false;
+ if (key === 'GOOGLE_REFRESH_TOKEN') {
+ initialValues[key].required = false;
}
});
setEnvValues(initialValues);
@@ -88,15 +143,63 @@ export const MCPEnvDialog: FC = ({
onClose();
};
- const handleConfigureMcpEnvSetting = () => {
- setEnvValues({});
- setShowKeys({});
- const mcp = { ...activeMcp };
+ const setFieldError = (key: string, error: string) => {
+ setEnvValues((prev) => ({
+ ...prev,
+ [key]: {
+ ...prev[key],
+ error,
+ },
+ }));
+ };
+
+ const clearFieldErrors = () => {
+ setEnvValues((prev) => {
+ const updated: typeof prev = {};
+ Object.keys(prev).forEach((key) => {
+ updated[key] = { ...prev[key], error: "" };
+ });
+ return updated;
+ });
+ };
+
+ const handleConfigureMcpEnvSetting = async () => {
+ if (isValidating) return;
+
+ setIsValidating(true);
+ clearFieldErrors();
+
const env: { [key: string]: string } = {};
- Object.keys(envValues).map((key) => {
+ Object.keys(envValues).forEach((key) => {
env[key] = envValues[key]?.value;
});
- mcp.install_command.env = env;
+
+ // Validate Google API key
+ if (env["GOOGLE_API_KEY"] && env["SEARCH_ENGINE_ID"]) {
+ const result = await google_check(env["GOOGLE_API_KEY"], env["SEARCH_ENGINE_ID"]);
+ if (!result.success) {
+ setFieldError("GOOGLE_API_KEY", result.message);
+ setFieldError("SEARCH_ENGINE_ID", result.message);
+ setIsValidating(false);
+ return;
+ }
+ }
+
+ // Validate Exa API key
+ if (env["EXA_API_KEY"]) {
+ const result = await exa_check(env["EXA_API_KEY"]);
+ if (!result.success) {
+ setFieldError("EXA_API_KEY", result.message);
+ setIsValidating(false);
+ return;
+ }
+ }
+
+ // Save only if all validations succeed
+ const mcp = { ...activeMcp, install_command: { ...activeMcp.install_command, env } };
+ setEnvValues({});
+ setShowKeys({});
+ setIsValidating(false);
onConnect(mcp);
};
return (
@@ -112,7 +215,7 @@ export const MCPEnvDialog: FC = ({
- {t("setting.configure {name} Toolkit", {name: activeMcp?.name})}
+ {t("setting.configure {name} Toolkit", { name: activeMcp?.name })}
@@ -152,7 +255,7 @@ export const MCPEnvDialog: FC = ({
{Object.keys(activeMcp?.install_command?.env || {}).map((key) => (
- {key}{envValues[key]?.required&&'*'}
+ {key}{envValues[key]?.required && '*'}
= ({
{envValues[key]?.tip}
+ {envValues[key]?.error && (
+ {envValues[key]?.error}
+ )}
{key === 'SEARCH_ENGINE_ID' && (
- {t("setting.get-it-from")}: {
+ {t("setting.get-it-from")}: {
window.location.href = "https://developers.google.com/custom-search/v1/overview";
}} className="underline text-blue-500">{t("setting.google-custom-search-api")}
)}
{key === 'GOOGLE_API_KEY' && (
- {t("setting.get-it-from")}: {
+ {t("setting.get-it-from")}: {
window.location.href = "https://console.cloud.google.com/apis/credentials";
}} className="underline text-blue-500">{t("setting.google-cloud-console")}
)}
{key === 'EXA_API_KEY' && (
- {t("setting.get-it-from")}: {
+ {t("setting.get-it-from")}: {
window.location.href = "https://exa.ai";
}} className="underline text-blue-500">Exa.ai (Optional)
@@ -213,8 +319,9 @@ export const MCPEnvDialog: FC = ({
onClick={handleConfigureMcpEnvSetting}
variant="primary"
size="md"
+ disabled={isValidating}
>
- {t("setting.connect")}
+ {isValidating ? "Validating..." : t("setting.connect")}
diff --git a/src/pages/Setting/components/MCPListItem.tsx b/src/pages/Setting/components/MCPListItem.tsx
index 8f66e4e96..434b35826 100644
--- a/src/pages/Setting/components/MCPListItem.tsx
+++ b/src/pages/Setting/components/MCPListItem.tsx
@@ -2,9 +2,7 @@ import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Ellipsis, Settings, Trash2, CircleAlert } from "lucide-react";
import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
+ TooltipSimple
} from "@/components/ui/tooltip";
import {
Popover,
@@ -41,14 +39,9 @@ export default function MCPListItem({
{item.mcp_name}
-
-
+
-
-
- {item.mcp_desc}
-
-
+
diff --git a/src/store/authStore.ts b/src/store/authStore.ts
index c1810509c..63850e82e 100644
--- a/src/store/authStore.ts
+++ b/src/store/authStore.ts
@@ -4,7 +4,7 @@ import { persist } from 'zustand/middleware';
// type definition
type InitState = 'permissions' | 'carousel' | 'done';
type ModelType = 'cloud' | 'local' | 'custom';
-type CloudModelType = 'gemini/gemini-2.5-pro' | 'gemini-2.5-flash' | 'gpt-4.1-mini' | 'gpt-4.1' | 'claude-opus-4-1-20250805' | 'claude-sonnet-4-20250514' | 'claude-3-5-haiku-20241022' | 'gpt-5' | 'gpt-5-mini' | 'gpt-5-nano';
+type CloudModelType = 'gemini/gemini-2.5-pro' | 'gemini-2.5-flash' | 'gpt-4.1-mini' | 'gpt-4.1' | 'claude-sonnet-4-5' | 'claude-3-5-haiku-20241022' | 'gpt-5' | 'gpt-5-mini' | 'gpt-5-nano';
// auth info interface
interface AuthInfo {
@@ -169,7 +169,7 @@ const EMPTY_LIST: Agent[] = [];
// worker list Hook
export const useWorkerList = (): Agent[] => {
- const { email } = getAuthStore();
- const workerList = getAuthStore().workerListData[email as string];
+ const { email, workerListData } = getAuthStore();
+ const workerList = workerListData[email as string];
return workerList ?? EMPTY_LIST;
};
\ No newline at end of file
diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts
index 00dfcbbb5..c0f035911 100644
--- a/src/store/chatStore.ts
+++ b/src/store/chatStore.ts
@@ -489,7 +489,7 @@ const chatStore = create()(
}
if (targetTaskIndex !== -1) {
- console.log("targetTaskIndex", targetTaskIndex,state)
+ console.log("targetTaskIndex", targetTaskIndex, state)
taskRunning[targetTaskIndex].status = state === "DONE" ? "completed" : "failed";
}
setTaskRunning(taskId, taskRunning)
@@ -612,17 +612,16 @@ const chatStore = create()(
}
- // If the state is "waiting", only mark it in the agent's task list and do not add it to taskRunning
+ // Handle task assignment to taskAssigning based on state
if (taskState === "waiting") {
if (!taskAssigning[assigneeAgentIndex].tasks.find(item => item.id === task_id)) {
taskAssigning[assigneeAgentIndex].tasks.push(task ?? { id: task_id, content, status: "waiting" });
}
setTaskAssigning(taskId, [...taskAssigning]);
- return; // Return early, do not add to the running queue
- }
+ }
// The following logic is for when the task actually starts executing (running)
- if (taskAssigning && taskAssigning[assigneeAgentIndex]) {
+ else if (taskAssigning && taskAssigning[assigneeAgentIndex]) {
// Check if task already exists in the agent's task list
const existingTaskIndex = taskAssigning[assigneeAgentIndex].tasks.findIndex(item => item.id === task_id);
@@ -649,11 +648,14 @@ const chatStore = create()(
// Only update or add to taskRunning, never duplicate
if (taskRunningIndex === -1) {
// Task not in taskRunning, add it
+ if(task){
+ task.status = taskState === "waiting" ? "waiting" : "running";
+ }
taskRunning!.push(
task ?? {
id: task_id,
content,
- status: "",
+ status: taskState === "waiting" ? "waiting" : "running",
agent: JSON.parse(JSON.stringify(taskAgent)),
}
);
@@ -661,7 +663,7 @@ const chatStore = create()(
// Task already in taskRunning, update it
taskRunning![taskRunningIndex] = {
...taskRunning![taskRunningIndex],
- status: "",
+ status: taskState === "waiting" ? "waiting" : "running",
agent: JSON.parse(JSON.stringify(taskAgent)),
};
}
diff --git a/src/store/installationStore.ts b/src/store/installationStore.ts
new file mode 100644
index 000000000..374d88965
--- /dev/null
+++ b/src/store/installationStore.ts
@@ -0,0 +1,216 @@
+import { create } from 'zustand';
+import { subscribeWithSelector } from 'zustand/middleware';
+
+// Define all possible installation states
+export type InstallationState =
+ | 'idle'
+ | 'checking-permissions'
+ | 'showing-carousel'
+ | 'installing'
+ | 'error'
+ | 'completed';
+
+// Installation log entry
+export interface InstallationLog {
+ type: 'stdout' | 'stderr';
+ data: string;
+ timestamp: Date;
+}
+
+// Installation store state
+interface InstallationStoreState {
+ // Core state
+ state: InstallationState;
+ progress: number;
+ logs: InstallationLog[];
+ error?: string;
+ isVisible: boolean;
+
+ // Actions
+ startInstallation: () => void;
+ addLog: (log: InstallationLog) => void;
+ setSuccess: () => void;
+ setError: (error: string) => void;
+ retryInstallation: () => void;
+ completeSetup: () => void;
+ updateProgress: (progress: number) => void;
+ setVisible: (visible: boolean) => void;
+ reset: () => void;
+
+ // Async actions
+ performInstallation: () => Promise;
+ exportLog: () => Promise;
+}
+
+// Initial state
+const initialState = {
+ state: 'idle' as InstallationState,
+ progress: 20,
+ logs: [] as InstallationLog[],
+ error: undefined,
+ isVisible: false,
+};
+
+// Create the installation store
+export const useInstallationStore = create()(
+ subscribeWithSelector(
+ (set, get) => ({
+ // Initial state
+ ...initialState,
+
+ // Basic actions
+ startInstallation: () =>
+ set({
+ state: 'installing',
+ progress: 20,
+ logs: [],
+ error: undefined,
+ isVisible: true,
+ }),
+
+ addLog: (log: InstallationLog) =>
+ set((state) => {
+ const newProgress = Math.min(state.progress + 5, 90);
+ return {
+ logs: [...state.logs, log],
+ progress: newProgress,
+ };
+ }),
+
+ setSuccess: () =>
+ set({
+ state: 'completed',
+ progress: 100,
+ }),
+
+ setError: (error: string) =>
+ set((state) => ({
+ state: 'error',
+ error,
+ logs: [
+ ...state.logs,
+ {
+ type: 'stderr',
+ data: error,
+ timestamp: new Date(),
+ },
+ ],
+ })),
+
+ retryInstallation: () => {
+ set({
+ ...initialState,
+ isVisible: true,
+ state: 'installing',
+ });
+ get().performInstallation();
+ },
+
+ completeSetup: () =>
+ set({
+ state: 'completed',
+ isVisible: false,
+ }),
+
+ updateProgress: (progress: number) =>
+ set({ progress }),
+
+ setVisible: (visible: boolean) =>
+ set({ isVisible: visible }),
+
+ reset: () =>
+ set(initialState),
+
+ // Async actions
+ performInstallation: async () => {
+ const { startInstallation, setSuccess, setError } = get();
+
+ try {
+ startInstallation();
+ const result = await window.electronAPI.checkAndInstallDepsOnUpdate();
+
+ if (result.success) {
+ setSuccess();
+ // Update auth store
+ const { useAuthStore } = await import('./authStore');
+ useAuthStore.getState().setInitState('done');
+ } else {
+ setError('Installation failed');
+ }
+ } catch (error) {
+ setError(error instanceof Error ? error.message : 'Unknown error');
+ }
+ },
+
+ exportLog: async () => {
+ try {
+ const response = await window.electronAPI.exportLog();
+
+ if (!response.success) {
+ alert('Export cancelled: ' + response.error);
+ return;
+ }
+
+ if (response.savedPath) {
+ window.location.href = 'https://github.com/eigent-ai/eigent/issues/new/choose';
+ alert('Log saved: ' + response.savedPath);
+ }
+ } catch (e: any) {
+ alert('Export error: ' + e.message);
+ }
+ },
+ })
+ )
+);
+
+// Computed selectors
+export const useLatestLog = () => useInstallationStore(state =>
+ state.logs[state.logs.length - 1]
+);
+
+export const useInstallationActions = () => useInstallationStore(state => ({
+ startInstallation: state.startInstallation,
+ retryInstallation: state.retryInstallation,
+ completeSetup: state.completeSetup,
+ performInstallation: state.performInstallation,
+ exportLog: state.exportLog,
+}));
+
+// Combined hook for components that need multiple pieces of state
+export const useInstallationStatus = () => {
+ const state = useInstallationStore(state => state.state);
+ const isVisible = useInstallationStore(state => state.isVisible);
+
+ return {
+ isInstalling: state === 'installing',
+ installationState: state,
+ shouldShowInstallScreen: isVisible && state !== 'completed',
+ isInstallationComplete: state === 'completed',
+ canRetry: state === 'error',
+ };
+};
+
+// Hook for the main installation UI component
+export const useInstallationUI = () => {
+ const state = useInstallationStore(state => state.state);
+ const progress = useInstallationStore(state => state.progress);
+ const logs = useInstallationStore(state => state.logs);
+ const error = useInstallationStore(state => state.error);
+ const isVisible = useInstallationStore(state => state.isVisible);
+ const performInstallation = useInstallationStore(state => state.performInstallation);
+ const retryInstallation = useInstallationStore(state => state.retryInstallation);
+ const exportLog = useInstallationStore(state => state.exportLog);
+
+ return {
+ installationState: state,
+ progress,
+ latestLog: logs[logs.length - 1],
+ error,
+ isInstalling: state === 'installing',
+ shouldShowInstallScreen: isVisible && state !== 'completed',
+ canRetry: state === 'error',
+ performInstallation,
+ retryInstallation,
+ exportLog,
+ };
+};
\ No newline at end of file
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts
index e1319bac5..470529c9e 100644
--- a/src/types/electron.d.ts
+++ b/src/types/electron.d.ts
@@ -45,9 +45,16 @@ interface ElectronAPI {
envRemove: (email: string, key: string) => Promise;
getEnvPath: (email: string) => Promise;
executeCommand: (command: string,email:string) => Promise<{ success: boolean; stdout?: string; stderr?: string; error?: string }>;
- installDependencies: () => Promise<{ success: boolean; error?: string }>;
- frontendReady: () => Promise<{ success: boolean; error?: string }>;
+ checkAndInstallDepsOnUpdate: () => Promise<{ success: boolean; error?: string }>;
checkInstallBrowser: () => Promise<{ data:any[] }>;
+ getInstallationStatus: () => Promise<{
+ success: boolean;
+ isInstalling?: boolean;
+ hasLockFile?: boolean;
+ installedExists?: boolean;
+ timestamp?: number;
+ error?: string
+ }>;
onInstallDependenciesStart: (callback: () => void) => void;
onInstallDependenciesLog: (callback: (data: { type: string; data: string }) => void) => void;
onInstallDependenciesComplete: (callback: (data: { success: boolean; code?: number; error?: string }) => void) => void;
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 000000000..64d2cf8c2
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,423 @@
+# Installation Flow Testing Environment
+
+This comprehensive testing environment allows you to test all installation flows end-to-end with mocked `uv sync`, `uvicorn`, and Electron APIs. It simulates different system states and provides utilities to change the environment during tests.
+
+## Overview
+
+The testing environment consists of three main components:
+
+1. **Electron API Mocks** (`test/mocks/electronMocks.ts`) - Mock Electron's preload APIs
+2. **Environment State Mocks** (`test/mocks/environmentMocks.ts`) - Mock filesystem, processes, and system state
+3. **Test Scenarios** - Predefined scenarios for different installation flows
+
+## Quick Start
+
+```typescript
+import { setupElectronMocks, TestScenarios } from '../mocks/electronMocks'
+import { setupMockEnvironment } from '../mocks/environmentMocks'
+
+describe('My Installation Test', () => {
+ let electronAPI: MockedElectronAPI
+ let mockEnv: ReturnType
+
+ beforeEach(() => {
+ // Set up mocks
+ const { electronAPI: api } = setupElectronMocks()
+ electronAPI = api
+ mockEnv = setupMockEnvironment()
+ })
+
+ it('should handle version update', async () => {
+ // Apply scenario
+ TestScenarios.versionUpdate(electronAPI)
+
+ // Your test code here
+ })
+})
+```
+
+## Electron API Mocks
+
+### Available Mock Methods
+
+- `checkAndInstallDepsOnUpdate()` - Simulates dependency installation
+- `getInstallationStatus()` - Returns current installation status
+- `exportLog()` - Simulates log export functionality
+- Event listeners for installation events
+
+### Simulation Functions
+
+```typescript
+// Simulate installation events
+electronAPI.simulateInstallationStart()
+electronAPI.simulateInstallationLog('stdout', 'Installing packages...')
+electronAPI.simulateInstallationComplete(true) // or false for failure
+
+// Simulate system changes
+electronAPI.simulateVersionChange('2.0.0')
+electronAPI.simulateVenvRemoval()
+electronAPI.simulateUvicornStartup()
+```
+
+### Mock State Control
+
+```typescript
+// Control the mock state directly
+electronAPI.mockState.venvExists = false
+electronAPI.mockState.isInstalling = true
+electronAPI.mockState.toolInstalled = false
+```
+
+## Environment State Mocks
+
+### Filesystem Mock
+
+Controls file system operations:
+
+```typescript
+// Control file existence
+mockEnv.mockState.filesystem.venvExists = false
+mockEnv.mockState.filesystem.versionFileExists = true
+mockEnv.mockState.filesystem.installedLockExists = false
+
+// Control file contents
+mockEnv.mockState.filesystem.versionFileContent = '0.9.0'
+```
+
+### Process Mock
+
+Controls process spawning and execution:
+
+```typescript
+// Control tool availability
+mockEnv.mockState.processes.uvAvailable = false
+mockEnv.mockState.processes.bunAvailable = true
+mockEnv.mockState.processes.uvicornRunning = false
+
+// Control network connectivity
+mockEnv.mockState.network.canConnectToDefault = false
+mockEnv.mockState.network.canConnectToMirror = true
+```
+
+## Predefined Test Scenarios
+
+### Electron API Scenarios
+
+Use `TestScenarios` from `electronMocks.ts`:
+
+```typescript
+// Fresh installation - no .venv, no version file
+TestScenarios.freshInstall(electronAPI)
+
+// Version update - version file exists but version changed
+TestScenarios.versionUpdate(electronAPI)
+
+// .venv removed - version file exists but .venv is missing
+TestScenarios.venvRemoved(electronAPI)
+
+// Installation in progress - when user opens app during installation
+TestScenarios.installationInProgress(electronAPI)
+
+// Installation error scenario
+TestScenarios.installationError(electronAPI)
+
+// Uvicorn startup with dependency installation
+TestScenarios.uvicornDepsInstall(electronAPI)
+
+// All good - no installation needed
+TestScenarios.allGood(electronAPI)
+```
+
+### Environment Scenarios
+
+Use `mockEnv.scenarios` from `environmentMocks.ts`:
+
+```typescript
+// Fresh installation
+mockEnv.scenarios.freshInstall()
+
+// Version update
+mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
+
+// .venv removed
+mockEnv.scenarios.venvRemoved()
+
+// Network issues
+mockEnv.scenarios.networkIssues()
+
+// Complete failure
+mockEnv.scenarios.completeFailure()
+
+// Uvicorn startup installation
+mockEnv.scenarios.uvicornStartupInstall()
+
+// Installation in progress
+mockEnv.scenarios.installationInProgress()
+```
+
+## Testing Different Installation States
+
+### Installation Store States
+
+Test all possible states from `installationStore.ts`:
+
+- `'idle'` - Initial state
+- `'checking-permissions'` - Checking system permissions
+- `'showing-carousel'` - Showing onboarding carousel
+- `'installing'` - Installation in progress
+- `'error'` - Installation failed
+- `'completed'` - Installation successful
+
+```typescript
+import { useInstallationStore } from '@/store/installationStore'
+
+it('should transition through all states', () => {
+ const store = useInstallationStore.getState()
+
+ expect(store.state).toBe('idle')
+
+ store.startInstallation()
+ expect(store.state).toBe('installing')
+
+ store.setError('Installation failed')
+ expect(store.state).toBe('error')
+
+ store.retryInstallation()
+ expect(store.state).toBe('installing')
+
+ store.setSuccess()
+ expect(store.state).toBe('completed')
+})
+```
+
+## Specific Test Cases
+
+### 1. Testing .venv Removal
+
+```typescript
+it('should handle .venv removal', async () => {
+ // Simulate .venv being removed
+ TestScenarios.venvRemoved(electronAPI)
+ // or
+ mockEnv.scenarios.venvRemoved()
+
+ // Test your component/hook
+ const result = await electronAPI.checkAndInstallDepsOnUpdate()
+ expect(result.success).toBe(true)
+})
+```
+
+### 2. Testing Version File Changes
+
+```typescript
+it('should handle version file changes', async () => {
+ // Simulate version change
+ TestScenarios.versionUpdate(electronAPI)
+ // or
+ mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
+
+ // Your test assertions
+})
+```
+
+### 3. Testing Uvicorn Startup Installation
+
+```typescript
+it('should handle uvicorn starting with dependency installation', async () => {
+ // Simulate uvicorn detecting missing dependencies
+ TestScenarios.uvicornDepsInstall(electronAPI)
+
+ // Trigger uvicorn startup
+ electronAPI.simulateUvicornStartup()
+
+ // Wait for installation events
+ await waitFor(() => {
+ expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
+ })
+})
+```
+
+### 4. Testing UI Installation States
+
+```typescript
+it('should show correct UI for each installation state', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ // Test idle state
+ expect(result.current.state).toBe('idle')
+ expect(result.current.isVisible).toBe(false)
+
+ // Test installing state
+ act(() => result.current.startInstallation())
+ expect(result.current.state).toBe('installing')
+ expect(result.current.isVisible).toBe(true)
+
+ // Test error state
+ act(() => result.current.setError('Installation failed'))
+ expect(result.current.state).toBe('error')
+ expect(result.current.error).toBe('Installation failed')
+
+ // Test completed state
+ act(() => result.current.setSuccess())
+ expect(result.current.state).toBe('completed')
+ expect(result.current.progress).toBe(100)
+})
+```
+
+## Advanced Testing Patterns
+
+### Testing Event Sequences
+
+```typescript
+it('should handle complete installation flow', async () => {
+ const events: string[] = []
+
+ // Set up event tracking
+ electronAPI.onInstallDependenciesStart(() => events.push('start'))
+ electronAPI.onInstallDependenciesLog(() => events.push('log'))
+ electronAPI.onInstallDependenciesComplete(() => events.push('complete'))
+
+ // Trigger installation
+ await electronAPI.checkAndInstallDepsOnUpdate()
+
+ // Verify event sequence
+ expect(events).toEqual(['start', 'log', 'log', 'complete'])
+})
+```
+
+### Testing Error Recovery
+
+```typescript
+it('should recover from installation errors', async () => {
+ // Set up error scenario
+ TestScenarios.installationError(electronAPI)
+
+ const store = useInstallationStore.getState()
+
+ // Trigger installation
+ await store.performInstallation()
+ expect(store.state).toBe('error')
+
+ // Simulate retry
+ TestScenarios.allGood(electronAPI) // Fix the environment
+ store.retryInstallation()
+
+ await waitFor(() => {
+ expect(store.state).toBe('completed')
+ })
+})
+```
+
+### Testing Concurrent Operations
+
+```typescript
+it('should handle concurrent installation attempts', async () => {
+ const store = useInstallationStore.getState()
+
+ // Start multiple installations
+ const promise1 = store.performInstallation()
+ const promise2 = store.performInstallation()
+
+ // Should handle gracefully
+ const [result1, result2] = await Promise.all([promise1, promise2])
+
+ expect(store.state).toBe('completed')
+})
+```
+
+## Debugging Tests
+
+### Logging Mock State
+
+```typescript
+// Log current mock state
+console.log('Electron API State:', electronAPI.mockState)
+console.log('Environment State:', mockEnv.mockState)
+
+// Check what functions were called
+console.log('checkAndInstallDepsOnUpdate calls:',
+ electronAPI.checkAndInstallDepsOnUpdate.mock.calls)
+```
+
+### Waiting for Async Operations
+
+```typescript
+import { waitForStateChange } from '../mocks/environmentMocks'
+
+// Wait for specific state changes
+await waitForStateChange(
+ () => mockEnv.mockState.processes.uvSyncInProgress,
+ true,
+ 1000 // timeout
+)
+```
+
+## Running the Tests
+
+```bash
+# Run all installation tests
+npm test test/unit/store/installationStore.test.ts
+npm test test/unit/hooks/useInstallationSetup.test.ts
+npm test test/unit/electron/install-deps.test.ts
+
+# Run with coverage
+npm test -- --coverage
+
+# Run in watch mode
+npm test -- --watch
+```
+
+## Common Issues and Solutions
+
+### 1. Mock Not Applied
+
+**Problem**: Mock functions not being called
+**Solution**: Ensure mocks are set up before importing modules
+
+```typescript
+beforeEach(async () => {
+ setupMocks() // Set up first
+ const module = await import('./module') // Import after
+})
+```
+
+### 2. State Not Updating
+
+**Problem**: Mock state changes not reflected
+**Solution**: Use simulation functions instead of direct state mutation
+
+```typescript
+// Don't do this
+electronAPI.mockState.isInstalling = true
+
+// Do this instead
+electronAPI.simulateInstallationStart()
+```
+
+### 3. Async Operations Not Completing
+
+**Problem**: Tests timeout waiting for async operations
+**Solution**: Use proper wait functions and increase timeouts
+
+```typescript
+await vi.waitFor(() => {
+ expect(condition).toBe(true)
+}, { timeout: 2000 })
+```
+
+## Best Practices
+
+1. **Reset State**: Always reset mock state between tests
+2. **Use Scenarios**: Prefer predefined scenarios over manual state setup
+3. **Test Edge Cases**: Include error conditions and edge cases
+4. **Verify Events**: Check that the correct events are emitted
+5. **Test Cleanup**: Verify that resources are properly cleaned up
+6. **Integration Tests**: Test the complete flow, not just individual functions
+
+## Example Test Files
+
+- `test/unit/store/installationStore.test.ts` - Store state management
+- `test/unit/hooks/useInstallationSetup.test.ts` - Hook behavior
+- `test/unit/electron/install-deps.test.ts` - Backend installation logic
+
+These test files demonstrate all the patterns and scenarios described in this README.
\ No newline at end of file
diff --git a/test/mocks/electronMocks.ts b/test/mocks/electronMocks.ts
new file mode 100644
index 000000000..c2e28a91a
--- /dev/null
+++ b/test/mocks/electronMocks.ts
@@ -0,0 +1,467 @@
+import { vi } from 'vitest'
+
+export interface MockedElectronAPI {
+ // Mock environment state that can be controlled in tests
+ mockState: {
+ venvExists: boolean
+ versionFileExists: boolean
+ currentVersion: string
+ savedVersion: string
+ isInstalling: boolean
+ installedLockExists: boolean
+ uvicornStarting: boolean
+ toolInstalled: boolean
+ allowForceInstall: boolean
+ // Environment-related state
+ envFileExists: boolean
+ envContent: string
+ eigentDirExists: boolean
+ userEmail: string
+ mcpRemoteConfigExists: boolean
+ hasToken: boolean
+ }
+
+ // Mock implementation functions
+ checkAndInstallDepsOnUpdate: ReturnType
+ getInstallationStatus: ReturnType
+ exportLog: ReturnType
+ onInstallDependenciesStart: ReturnType
+ onInstallDependenciesLog: ReturnType
+ onInstallDependenciesComplete: ReturnType
+ removeAllListeners: ReturnType
+
+ // EnvUtil mock functions
+ getEnvPath: ReturnType
+ updateEnvBlock: ReturnType
+ removeEnvKey: ReturnType
+ getEmailFolderPath: ReturnType
+ parseEnvBlock: ReturnType
+
+ // Test utilities
+ simulateInstallationStart: () => void
+ simulateInstallationLog: (type: 'stdout' | 'stderr', data: string) => void
+ simulateInstallationComplete: (success: boolean, error?: string) => void
+ simulateVersionChange: (newVersion: string) => void
+ simulateVenvRemoval: () => void
+ simulateUvicornStartup: () => void
+ simulateEnvCorruption: () => void
+ simulateUserEmailChange: (email: string) => void
+ simulateMcpConfigMissing: () => void
+ reset: () => void
+}
+
+export interface MockedIpcRenderer {
+ invoke: ReturnType
+ on: ReturnType
+ removeAllListeners: ReturnType
+}
+
+/**
+ * Creates a comprehensive mock for the Electron API
+ * This mock can simulate all the different installation scenarios
+ */
+export function createElectronAPIMock(): MockedElectronAPI {
+ // Listeners for simulation
+ const installStartListeners: Array<() => void> = []
+ const installLogListeners: Array<(data: { type: string; data: string }) => void> = []
+ const installCompleteListeners: Array<(data: { success: boolean; code?: number; error?: string }) => void> = []
+
+ const mockState = {
+ venvExists: true,
+ versionFileExists: true,
+ currentVersion: '1.0.0',
+ savedVersion: '1.0.0',
+ isInstalling: false,
+ installedLockExists: true,
+ uvicornStarting: false,
+ toolInstalled: true,
+ allowForceInstall: false,
+ // Environment-related state
+ envFileExists: true,
+ envContent: 'MOCK_VAR=mock_value\n# === MCP INTEGRATION ENV START ===\nMCP_KEY=test_value\n# === MCP INTEGRATION ENV END ===',
+ eigentDirExists: true,
+ userEmail: 'test@example.com',
+ mcpRemoteConfigExists: true,
+ hasToken: true,
+ }
+
+ const electronAPI: MockedElectronAPI = {
+ mockState,
+
+ // Core API functions
+ checkAndInstallDepsOnUpdate: vi.fn().mockImplementation(async () => {
+ const { versionFileExists, currentVersion, savedVersion, allowForceInstall, venvExists, toolInstalled } = mockState
+
+ // Simulate the real implementation logic that checks:
+ // 1. Version file existence and version match
+ // 2. Virtual environment existence
+ // 3. Command tools installation status
+ const versionChanged = !versionFileExists || savedVersion !== currentVersion
+ const needsInstallation = allowForceInstall || versionChanged || !venvExists || !toolInstalled
+
+ if (needsInstallation) {
+ // Log the reason for installation
+ if (!toolInstalled) {
+ electronAPI.simulateInstallationLog('stdout', 'Command tools missing, starting installation...')
+ } else if (!venvExists) {
+ electronAPI.simulateInstallationLog('stdout', 'Virtual environment missing, starting installation...')
+ } else if (versionChanged) {
+ electronAPI.simulateInstallationLog('stdout', 'Version changed, starting installation...')
+ }
+
+ // Trigger installation
+ electronAPI.simulateInstallationStart()
+
+ // Simulate installation process with delay
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stdout', 'Installing packages...')
+ setTimeout(() => {
+ electronAPI.simulateInstallationComplete(true)
+ // Update state after successful installation
+ mockState.venvExists = true
+ mockState.toolInstalled = true
+ mockState.installedLockExists = true
+ }, 100)
+ }, 100)
+ }, 50)
+
+ return { success: true, message: 'Dependencies installed successfully after update' }
+ } else {
+ return { success: true, message: 'Version not changed, venv exists, and tools installed - skipped installation' }
+ }
+ }),
+
+ getInstallationStatus: vi.fn().mockImplementation(async () => {
+ return {
+ success: true,
+ isInstalling: mockState.isInstalling,
+ hasLockFile: mockState.isInstalling || mockState.installedLockExists,
+ installedExists: mockState.installedLockExists
+ }
+ }),
+
+ exportLog: vi.fn().mockImplementation(async () => {
+ return {
+ success: true,
+ savedPath: '/mock/path/to/log.txt'
+ }
+ }),
+
+ // Event listeners
+ onInstallDependenciesStart: vi.fn().mockImplementation((callback: () => void) => {
+ installStartListeners.push(callback)
+ }),
+
+ onInstallDependenciesLog: vi.fn().mockImplementation((callback: (data: { type: string; data: string }) => void) => {
+ installLogListeners.push(callback)
+ }),
+
+ onInstallDependenciesComplete: vi.fn().mockImplementation((callback: (data: { success: boolean; code?: number; error?: string }) => void) => {
+ installCompleteListeners.push(callback)
+ }),
+
+ removeAllListeners: vi.fn().mockImplementation(() => {
+ installStartListeners.length = 0
+ installLogListeners.length = 0
+ installCompleteListeners.length = 0
+ }),
+
+ // EnvUtil mock functions
+ getEnvPath: vi.fn().mockImplementation((email: string) => {
+ const sanitizedEmail = email.split("@")[0].replace(/[\\/*?:"<>|\s]/g, "_").replace(".", "_")
+ return `/mock/home/.eigent/.env.${sanitizedEmail}`
+ }),
+
+ updateEnvBlock: vi.fn().mockImplementation((lines: string[], kv: Record) => {
+ // Mock implementation that adds/updates environment variables in the MCP block
+ const startMarker = '# === MCP INTEGRATION ENV START ==='
+ const endMarker = '# === MCP INTEGRATION ENV END ==='
+
+ let start = lines.findIndex(l => l.trim() === startMarker)
+ let end = lines.findIndex(l => l.trim() === endMarker)
+
+ if (start === -1 || end === -1) {
+ // No block exists, create one
+ lines.push(startMarker)
+ Object.entries(kv).forEach(([k, v]) => {
+ lines.push(`${k}=${v}`)
+ })
+ lines.push(endMarker)
+ return lines
+ }
+
+ // Update existing block
+ const newBlock = Object.entries(kv).map(([k, v]) => `${k}=${v}`)
+ return [
+ ...lines.slice(0, start + 1),
+ ...newBlock,
+ ...lines.slice(end)
+ ]
+ }),
+
+ removeEnvKey: vi.fn().mockImplementation((lines: string[], key: string) => {
+ // Mock implementation that removes a key from the MCP block
+ const startMarker = '# === MCP INTEGRATION ENV START ==='
+ const endMarker = '# === MCP INTEGRATION ENV END ==='
+
+ let start = lines.findIndex(l => l.trim() === startMarker)
+ let end = lines.findIndex(l => l.trim() === endMarker)
+
+ if (start === -1 || end === -1) return lines
+
+ const block = lines.slice(start + 1, end)
+ const newBlock = block.filter(line => !line.startsWith(key + '='))
+
+ return [
+ ...lines.slice(0, start + 1),
+ ...newBlock,
+ ...lines.slice(end)
+ ]
+ }),
+
+ getEmailFolderPath: vi.fn().mockImplementation((email: string) => {
+ const sanitizedEmail = email.split("@")[0].replace(/[\\/*?:"<>|\s]/g, "_").replace(".", "_")
+ return {
+ MCP_REMOTE_CONFIG_DIR: `/mock/home/.eigent/${sanitizedEmail}`,
+ MCP_CONFIG_DIR: '/mock/home/.eigent',
+ tempEmail: sanitizedEmail,
+ hasToken: mockState.hasToken
+ }
+ }),
+
+ parseEnvBlock: vi.fn().mockImplementation((content: string) => {
+ const lines = content.split(/\r?\n/)
+ const startMarker = '# === MCP INTEGRATION ENV START ==='
+ const endMarker = '# === MCP INTEGRATION ENV END ==='
+
+ let start = lines.findIndex(l => l.trim() === startMarker)
+ let end = lines.findIndex(l => l.trim() === endMarker)
+
+ if (start === -1) start = lines.length
+ if (end === -1) end = lines.length
+
+ return { lines, start, end }
+ }),
+
+ // Simulation utilities
+ simulateInstallationStart: () => {
+ mockState.isInstalling = true
+ installStartListeners.forEach(listener => listener())
+ },
+
+ simulateInstallationLog: (type: 'stdout' | 'stderr', data: string) => {
+ installLogListeners.forEach(listener => listener({ type, data }))
+ },
+
+ simulateInstallationComplete: (success: boolean, error?: string) => {
+ mockState.isInstalling = false
+ if (success) {
+ mockState.installedLockExists = true
+ }
+ installCompleteListeners.forEach(listener =>
+ listener({ success, error, code: success ? 0 : 1 })
+ )
+ },
+
+ simulateVersionChange: (newVersion: string) => {
+ mockState.currentVersion = newVersion
+ // This simulates a version mismatch scenario
+ },
+
+ simulateVenvRemoval: () => {
+ mockState.venvExists = false
+ mockState.installedLockExists = false
+ // Don't remove version file - this simulates venv being deleted but version file still existing
+ },
+
+ simulateUvicornStartup: () => {
+ mockState.uvicornStarting = true
+ // Simulate uvicorn detecting dependency installation need
+ setTimeout(() => {
+ electronAPI.simulateInstallationStart()
+ electronAPI.simulateInstallationLog('stdout', 'Uvicorn detected missing dependencies')
+ electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stdout', 'Uvicorn running on http://127.0.0.1:8000')
+ electronAPI.simulateInstallationComplete(true)
+ mockState.uvicornStarting = false
+ }, 200)
+ }, 100)
+ },
+
+ simulateEnvCorruption: () => {
+ mockState.envFileExists = true
+ mockState.envContent = 'INVALID_ENV_CONTENT\n# === MCP INTEGRATION ENV START ===\nBROKEN'
+ },
+
+ simulateUserEmailChange: (email: string) => {
+ mockState.userEmail = email
+ },
+
+ simulateMcpConfigMissing: () => {
+ mockState.mcpRemoteConfigExists = false
+ },
+
+ reset: () => {
+ Object.assign(mockState, {
+ venvExists: true,
+ versionFileExists: true,
+ currentVersion: '1.0.0',
+ savedVersion: '1.0.0',
+ isInstalling: false,
+ installedLockExists: true,
+ uvicornStarting: false,
+ toolInstalled: true,
+ allowForceInstall: false,
+ // Reset environment-related state
+ envFileExists: true,
+ envContent: 'MOCK_VAR=mock_value\n# === MCP INTEGRATION ENV START ===\nMCP_KEY=test_value\n# === MCP INTEGRATION ENV END ===',
+ eigentDirExists: true,
+ userEmail: 'test@example.com',
+ mcpRemoteConfigExists: true,
+ hasToken: true,
+ })
+
+ // Clear all listeners
+ installStartListeners.length = 0
+ installLogListeners.length = 0
+ installCompleteListeners.length = 0
+
+ // Reset all mocks
+ electronAPI.checkAndInstallDepsOnUpdate.mockClear()
+ electronAPI.getInstallationStatus.mockClear()
+ electronAPI.exportLog.mockClear()
+ electronAPI.onInstallDependenciesStart.mockClear()
+ electronAPI.onInstallDependenciesLog.mockClear()
+ electronAPI.onInstallDependenciesComplete.mockClear()
+ electronAPI.removeAllListeners.mockClear()
+ electronAPI.getEnvPath.mockClear()
+ electronAPI.updateEnvBlock.mockClear()
+ electronAPI.removeEnvKey.mockClear()
+ electronAPI.getEmailFolderPath.mockClear()
+ electronAPI.parseEnvBlock.mockClear()
+ }
+ }
+
+ return electronAPI
+}
+
+/**
+ * Creates a mock for the IPC Renderer
+ */
+export function createIpcRendererMock(): MockedIpcRenderer {
+ return {
+ invoke: vi.fn().mockImplementation(async (channel: string, ...args: any[]) => {
+ if (channel === 'check-tool-installed') {
+ return {
+ success: true,
+ isInstalled: true // This can be controlled via the electronAPI mock
+ }
+ }
+ return { success: false, error: 'Unknown channel' }
+ }),
+
+ on: vi.fn(),
+ removeAllListeners: vi.fn(),
+ }
+}
+
+/**
+ * Test utility to set up all Electron mocks
+ */
+export function setupElectronMocks() {
+ const electronAPI = createElectronAPIMock()
+ const ipcRenderer = createIpcRendererMock()
+
+ // Set up global mocks
+ Object.defineProperty(window, 'electronAPI', {
+ value: electronAPI,
+ writable: true
+ })
+
+ Object.defineProperty(window, 'ipcRenderer', {
+ value: ipcRenderer,
+ writable: true
+ })
+
+ return { electronAPI, ipcRenderer }
+}
+
+/**
+ * Predefined test scenarios
+ */
+export const TestScenarios = {
+ /**
+ * Fresh installation - no venv, no version file
+ */
+ freshInstall: (electronAPI: MockedElectronAPI) => {
+ electronAPI.mockState.venvExists = false
+ electronAPI.mockState.versionFileExists = false
+ electronAPI.mockState.installedLockExists = false
+ electronAPI.mockState.toolInstalled = false
+ },
+
+ /**
+ * Version update scenario - version file exists but version changed
+ */
+ versionUpdate: (electronAPI: MockedElectronAPI) => {
+ electronAPI.mockState.versionFileExists = true
+ electronAPI.mockState.savedVersion = '0.9.0'
+ electronAPI.mockState.currentVersion = '1.0.0'
+ electronAPI.mockState.installedLockExists = false
+ },
+
+ /**
+ * Venv removed scenario - version file exists but .venv is missing
+ */
+ venvRemoved: (electronAPI: MockedElectronAPI) => {
+ electronAPI.mockState.venvExists = false
+ electronAPI.mockState.versionFileExists = true
+ electronAPI.mockState.installedLockExists = false
+ },
+
+ /**
+ * Installation in progress - when user opens app during installation
+ */
+ installationInProgress: (electronAPI: MockedElectronAPI) => {
+ electronAPI.mockState.isInstalling = true
+ electronAPI.mockState.installedLockExists = false
+ },
+
+ /**
+ * Installation error scenario
+ */
+ installationError: (electronAPI: MockedElectronAPI) => {
+ electronAPI.checkAndInstallDepsOnUpdate.mockImplementation(async () => {
+ electronAPI.simulateInstallationStart()
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stderr', 'Error: Failed to resolve dependencies')
+ electronAPI.simulateInstallationComplete(false, 'Installation failed')
+ }, 100)
+ return { success: false, message: 'Installation failed' }
+ })
+ },
+
+ /**
+ * Uvicorn startup with dependency installation
+ */
+ uvicornDepsInstall: (electronAPI: MockedElectronAPI) => {
+ electronAPI.mockState.uvicornStarting = true
+ electronAPI.mockState.isInstalling = false
+ // The simulateUvicornStartup method will handle the rest
+ },
+
+ /**
+ * All good - no installation needed
+ */
+ allGood: (electronAPI: MockedElectronAPI) => {
+ electronAPI.mockState.venvExists = true
+ electronAPI.mockState.versionFileExists = true
+ electronAPI.mockState.savedVersion = electronAPI.mockState.currentVersion
+ electronAPI.mockState.installedLockExists = true
+ electronAPI.mockState.isInstalling = false
+ electronAPI.mockState.toolInstalled = true
+ }
+}
\ No newline at end of file
diff --git a/test/mocks/environmentMocks.ts b/test/mocks/environmentMocks.ts
new file mode 100644
index 000000000..92229db0d
--- /dev/null
+++ b/test/mocks/environmentMocks.ts
@@ -0,0 +1,687 @@
+import { vi } from 'vitest'
+
+/**
+ * Environment state management for testing installation flows
+ * This module provides utilities to simulate different system states
+ */
+
+export interface MockEnvironmentState {
+ filesystem: {
+ venvExists: boolean
+ versionFileExists: boolean
+ versionFileContent: string
+ installingLockExists: boolean
+ installedLockExists: boolean
+ backendPathExists: boolean
+ pyprojectExists: boolean
+ // New fields for process.ts functions
+ eigentDirExists: boolean
+ eigentBinDirExists: boolean
+ eigentCacheDirExists: boolean
+ eigentVenvsDirExists: boolean
+ eigentRuntimeDirExists: boolean
+ resourcesDirExists: boolean
+ binariesExist: { [name: string]: boolean }
+ oldVenvsExist: string[] // List of old venv directories that exist
+ }
+ processes: {
+ uvAvailable: boolean
+ bunAvailable: boolean
+ uvicornRunning: boolean
+ uvSyncInProgress: boolean
+ installationInProgress: boolean
+ }
+ app: {
+ currentVersion: string
+ userData: string
+ appPath: string
+ isPackaged: boolean
+ resourcesPath: string
+ }
+ system: {
+ platform: 'win32' | 'darwin' | 'linux'
+ homedir: string
+ }
+ network: {
+ canConnectToMirror: boolean
+ canConnectToDefault: boolean
+ }
+}
+
+/**
+ * Mock implementations for Node.js fs module
+ */
+export function createFileSystemMock() {
+ const mockState: MockEnvironmentState = {
+ filesystem: {
+ venvExists: true,
+ versionFileExists: true,
+ versionFileContent: '1.0.0',
+ installingLockExists: false,
+ installedLockExists: true,
+ backendPathExists: true,
+ pyprojectExists: true,
+ eigentDirExists: true,
+ eigentBinDirExists: true,
+ eigentCacheDirExists: true,
+ eigentVenvsDirExists: true,
+ eigentRuntimeDirExists: true,
+ resourcesDirExists: true,
+ binariesExist: { 'uv': true, 'bun': true },
+ oldVenvsExist: []
+ },
+ processes: {
+ uvAvailable: true,
+ bunAvailable: true,
+ uvicornRunning: false,
+ uvSyncInProgress: false,
+ installationInProgress: false,
+ },
+ app: {
+ currentVersion: '1.0.0',
+ userData: '/mock/user/data',
+ appPath: '/mock/app/path',
+ isPackaged: false,
+ resourcesPath: '/mock/resources/path'
+ },
+ system: {
+ platform: 'win32',
+ homedir: '/mock/home'
+ },
+ network: {
+ canConnectToMirror: true,
+ canConnectToDefault: true,
+ }
+ }
+
+ const fsMock = {
+ existsSync: vi.fn().mockImplementation((path: string) => {
+ if (!path || typeof path !== 'string') return false
+ if (path.includes('version.txt')) return mockState.filesystem.versionFileExists
+ if (path.includes('uv_installing.lock')) return mockState.filesystem.installingLockExists
+ if (path.includes('uv_installed.lock')) return mockState.filesystem.installedLockExists
+ if (path.includes('.venv')) return mockState.filesystem.venvExists
+ if (path.includes('backend')) return mockState.filesystem.backendPathExists
+ if (path.includes('pyproject.toml')) return mockState.filesystem.pyprojectExists
+ if (path.includes('.eigent/bin') || path.includes('.eigent\\bin')) return mockState.filesystem.eigentBinDirExists
+ if (path.includes('.eigent/cache') || path.includes('.eigent\\cache')) return mockState.filesystem.eigentCacheDirExists
+ if (path.includes('.eigent/venvs') || path.includes('.eigent\\venvs')) return mockState.filesystem.eigentVenvsDirExists
+ if (path.includes('.eigent/runtime') || path.includes('.eigent\\runtime')) return mockState.filesystem.eigentRuntimeDirExists
+ if (path.includes('.eigent') && !path.includes('bin') && !path.includes('cache') && !path.includes('venvs') && !path.includes('runtime')) {
+ return mockState.filesystem.eigentDirExists
+ }
+ if (path.includes('resources')) return mockState.filesystem.resourcesDirExists
+ // Check for specific binaries
+ for (const [name, exists] of Object.entries(mockState.filesystem.binariesExist)) {
+ if (path.includes(name + '.exe') || path.endsWith(name)) {
+ return exists
+ }
+ }
+ // Check for old venv directories
+ for (const oldVenv of mockState.filesystem.oldVenvsExist) {
+ if (path.includes(oldVenv)) return true
+ }
+ return true
+ }),
+
+ readFileSync: vi.fn().mockImplementation((path: string, encoding?: string) => {
+ if (!path || typeof path !== 'string') return ''
+ if (path.includes('version.txt')) {
+ return mockState.filesystem.versionFileContent
+ }
+ if (path.includes('pyproject.toml')) {
+ return `
+[project]
+name = "backend"
+version = "1.0.0"
+dependencies = ["fastapi", "uvicorn"]
+ `
+ }
+ return ''
+ }),
+
+ writeFileSync: vi.fn().mockImplementation((path: string, content: string) => {
+ if (!path || typeof path !== 'string') return
+ if (path.includes('version.txt')) {
+ mockState.filesystem.versionFileContent = content
+ mockState.filesystem.versionFileExists = true
+ } else if (path.includes('uv_installing.lock')) {
+ mockState.filesystem.installingLockExists = true
+ } else if (path.includes('uv_installed.lock')) {
+ mockState.filesystem.installedLockExists = true
+ }
+ }),
+
+ unlinkSync: vi.fn().mockImplementation((path: string) => {
+ if (!path || typeof path !== 'string') return
+ if (path.includes('uv_installing.lock')) {
+ mockState.filesystem.installingLockExists = false
+ } else if (path.includes('uv_installed.lock')) {
+ mockState.filesystem.installedLockExists = false
+ } else if (path.includes('version.txt')) {
+ mockState.filesystem.versionFileExists = false
+ }
+ }),
+
+ mkdirSync: vi.fn().mockImplementation((path: string, options?: any) => {
+ if (!path || typeof path !== 'string') return
+ if (path.includes('backend')) {
+ mockState.filesystem.backendPathExists = true
+ } else if (path.includes('.eigent/bin') || path.includes('.eigent\\bin')) {
+ mockState.filesystem.eigentBinDirExists = true
+ } else if (path.includes('.eigent/cache') || path.includes('.eigent\\cache')) {
+ mockState.filesystem.eigentCacheDirExists = true
+ } else if (path.includes('.eigent/venvs') || path.includes('.eigent\\venvs')) {
+ mockState.filesystem.eigentVenvsDirExists = true
+ } else if (path.includes('.eigent/runtime') || path.includes('.eigent\\runtime')) {
+ mockState.filesystem.eigentRuntimeDirExists = true
+ } else if (path.includes('.eigent')) {
+ mockState.filesystem.eigentDirExists = true
+ }
+ }),
+
+ rmSync: vi.fn().mockImplementation((path: string, options?: any) => {
+ if (!path || typeof path !== 'string') return
+ // Handle cleanup of old venvs
+ for (let i = 0; i < mockState.filesystem.oldVenvsExist.length; i++) {
+ if (path.includes(mockState.filesystem.oldVenvsExist[i])) {
+ mockState.filesystem.oldVenvsExist.splice(i, 1)
+ break
+ }
+ }
+ }),
+
+ readdirSync: vi.fn().mockImplementation((path: string, options?: any) => {
+ if (!path || typeof path !== 'string') return []
+ if (path.includes('.eigent/venvs')) {
+ // Return old venv directories for cleanup testing
+ return mockState.filesystem.oldVenvsExist.map(venv => ({
+ name: venv,
+ isDirectory: () => true
+ }))
+ }
+ return []
+ }),
+
+ // State control methods
+ mockState,
+
+ reset: () => {
+ Object.assign(mockState, {
+ filesystem: {
+ venvExists: true,
+ versionFileExists: true,
+ versionFileContent: '1.0.0',
+ installingLockExists: false,
+ installedLockExists: true,
+ backendPathExists: true,
+ pyprojectExists: true,
+ eigentDirExists: true,
+ eigentBinDirExists: true,
+ eigentCacheDirExists: true,
+ eigentVenvsDirExists: true,
+ eigentRuntimeDirExists: true,
+ resourcesDirExists: true,
+ binariesExist: { 'uv': true, 'bun': true },
+ oldVenvsExist: []
+ },
+ processes: {
+ uvAvailable: true,
+ bunAvailable: true,
+ uvicornRunning: false,
+ uvSyncInProgress: false,
+ installationInProgress: false,
+ },
+ app: {
+ currentVersion: '1.0.0',
+ userData: '/mock/user/data',
+ appPath: '/mock/app/path',
+ isPackaged: false,
+ resourcesPath: '/mock/resources/path'
+ },
+ system: {
+ platform: 'win32',
+ homedir: '/mock/home'
+ },
+ network: {
+ canConnectToMirror: true,
+ canConnectToDefault: true,
+ }
+ })
+ }
+ }
+
+ return fsMock
+}
+
+/**
+ * Mock implementations for child_process spawn
+ */
+export function createProcessMock() {
+ const processMock = {
+ spawn: vi.fn(),
+ mockState: {} as MockEnvironmentState,
+
+ setupSpawnMock: (mockState: MockEnvironmentState) => {
+ processMock.mockState = mockState
+
+ processMock.spawn.mockImplementation((command: string, args: string[], options: any) => {
+ // Mock process events
+ const mockProcess = {
+ stdout: {
+ on: vi.fn().mockImplementation((event: string, callback: (data: Buffer) => void) => {
+ if (event === 'data') {
+ // Simulate different process outputs based on command
+ setTimeout(() => {
+ if (command.includes('uv') && args.includes('sync')) {
+ mockState.processes.uvSyncInProgress = true
+ callback(Buffer.from('Resolved 10 packages in 1.2s\n'))
+ setTimeout(() => {
+ callback(Buffer.from('Installing packages...\n'))
+ setTimeout(() => {
+ callback(Buffer.from('Installation complete\n'))
+ mockState.processes.uvSyncInProgress = false
+ }, 100)
+ }, 50)
+ } else if (command.includes('uvicorn')) {
+ mockState.processes.uvicornRunning = true
+ callback(Buffer.from('Uvicorn running on http://127.0.0.1:8000\n'))
+ }
+ }, 10)
+ }
+ })
+ },
+ stderr: {
+ on: vi.fn().mockImplementation((event: string, callback: (data: Buffer) => void) => {
+ if (event === 'data') {
+ // Simulate error scenarios
+ if (!mockState.processes.uvAvailable && command.includes('uv')) {
+ setTimeout(() => {
+ callback(Buffer.from('uv: command not found\n'))
+ }, 10)
+ }
+ }
+ })
+ },
+ on: vi.fn().mockImplementation((event: string, callback: (code: number) => void) => {
+ if (event === 'close') {
+ setTimeout(() => {
+ if (command.includes('uv') && args.includes('sync')) {
+ const exitCode = mockState.processes.uvAvailable &&
+ mockState.network.canConnectToDefault ? 0 : 1
+ callback(exitCode)
+ } else {
+ callback(0)
+ }
+ }, 150)
+ }
+ }),
+ kill: vi.fn()
+ }
+
+ return mockProcess
+ })
+ },
+
+ reset: () => {
+ processMock.spawn.mockReset()
+ }
+ }
+
+ return processMock
+}
+
+/**
+ * Mock for Electron app module
+ */
+export function createElectronAppMock() {
+ const appMock = {
+ getVersion: vi.fn(),
+ getPath: vi.fn(),
+ getAppPath: vi.fn(),
+ isPackaged: false,
+ mockState: {} as MockEnvironmentState,
+
+ setup: (mockState: MockEnvironmentState) => {
+ appMock.mockState = mockState
+ appMock.getVersion.mockReturnValue(mockState.app.currentVersion)
+ appMock.getAppPath.mockReturnValue(mockState.app.appPath)
+ appMock.isPackaged = mockState.app.isPackaged
+ appMock.getPath.mockImplementation((name: string) => {
+ if (name === 'userData') return mockState.app.userData
+ return '/mock/path'
+ })
+
+ // Mock process.resourcesPath for packaged apps
+ if (mockState.app.isPackaged) {
+ Object.defineProperty(process, 'resourcesPath', {
+ value: mockState.app.resourcesPath,
+ configurable: true
+ })
+ }
+ },
+
+ reset: () => {
+ appMock.getVersion.mockReset()
+ appMock.getPath.mockReset()
+ appMock.getAppPath.mockReset()
+ }
+ }
+
+ return appMock
+}
+
+/**
+ * Mock for OS module
+ */
+export function createOsMock() {
+ const osMock = {
+ homedir: vi.fn().mockReturnValue('/mock/home'),
+ mockState: {} as MockEnvironmentState,
+
+ setup: (mockState: MockEnvironmentState) => {
+ osMock.mockState = mockState
+ osMock.homedir.mockReturnValue(mockState.system.homedir || '/mock/home')
+ },
+
+ reset: () => {
+ osMock.homedir.mockReset()
+ osMock.homedir.mockReturnValue('/mock/home')
+ }
+ }
+
+ return osMock
+}
+
+/**
+ * Mock for path module
+ */
+export function createPathMock() {
+ return {
+ join: vi.fn((...args) => {
+ const validArgs = args.filter(arg => arg != null && arg !== undefined && arg !== '')
+ return validArgs.length > 0 ? validArgs.join(process.platform === 'win32' ? '\\' : '/') : ''
+ }),
+ resolve: vi.fn((...args) => {
+ const validArgs = args.filter(arg => arg != null && arg !== undefined && arg !== '')
+ return validArgs.length > 0 ? validArgs.join(process.platform === 'win32' ? '\\' : '/') : ''
+ }),
+ dirname: vi.fn((path: string) => {
+ if (!path || typeof path !== 'string') return ''
+ const parts = path.split(process.platform === 'win32' ? '\\' : '/')
+ return parts.slice(0, -1).join(process.platform === 'win32' ? '\\' : '/')
+ })
+ }
+}
+
+/**
+ * Mock for process utilities from electron/main/utils/process.ts
+ */
+export function createProcessUtilsMock() {
+ const utilsMock = {
+ getResourcePath: vi.fn(),
+ getBackendPath: vi.fn(),
+ runInstallScript: vi.fn(),
+ getBinaryName: vi.fn(),
+ getBinaryPath: vi.fn(),
+ getCachePath: vi.fn(),
+ getVenvPath: vi.fn(),
+ getVenvsBaseDir: vi.fn(),
+ cleanupOldVenvs: vi.fn(),
+ isBinaryExists: vi.fn(),
+ mockState: {} as MockEnvironmentState,
+
+ setup: (mockState: MockEnvironmentState) => {
+ utilsMock.mockState = mockState
+
+ utilsMock.getResourcePath.mockReturnValue(
+ `${mockState.app.appPath}/resources`
+ )
+
+ utilsMock.getBackendPath.mockReturnValue(
+ mockState.app.isPackaged
+ ? `${mockState.app.resourcesPath}/backend`
+ : `${mockState.app.appPath}/backend`
+ )
+
+ utilsMock.runInstallScript.mockImplementation(async (scriptPath: string) => {
+ // Simulate successful script execution and update binary state
+ if (scriptPath.includes('install-uv')) {
+ mockState.filesystem.binariesExist['uv'] = true
+ mockState.processes.uvAvailable = true
+ } else if (scriptPath.includes('install-bun')) {
+ mockState.filesystem.binariesExist['bun'] = true
+ mockState.processes.bunAvailable = true
+ }
+ return true
+ })
+
+ utilsMock.getBinaryName.mockImplementation(async (name: string) => {
+ return mockState.system.platform === 'win32' ? `${name}.exe` : name
+ })
+
+ utilsMock.getBinaryPath.mockImplementation(async (name?: string) => {
+ const binDir = `${mockState.system.homedir}/.eigent/bin`
+ if (!name) return binDir
+ const binaryName = mockState.system.platform === 'win32' ? `${name}.exe` : name
+ return `${binDir}/${binaryName}`
+ })
+
+ utilsMock.getCachePath.mockImplementation((folder: string) => {
+ return `${mockState.system.homedir}/.eigent/cache/${folder}`
+ })
+
+ utilsMock.getVenvPath.mockImplementation((version: string) => {
+ return `${mockState.system.homedir}/.eigent/venvs/backend-${version}`
+ })
+
+ utilsMock.getVenvsBaseDir.mockReturnValue(
+ `${mockState.system.homedir}/.eigent/venvs`
+ )
+
+ utilsMock.cleanupOldVenvs.mockImplementation(async (currentVersion: string) => {
+ // Simulate cleanup by removing old venvs from mock state
+ mockState.filesystem.oldVenvsExist = mockState.filesystem.oldVenvsExist.filter(
+ venv => venv.includes(`backend-${currentVersion}`)
+ )
+ })
+
+ utilsMock.isBinaryExists.mockImplementation(async (name: string) => {
+ return mockState.filesystem.binariesExist[name] || false
+ })
+ },
+
+ reset: () => {
+ Object.values(utilsMock).forEach(fn => {
+ if (typeof fn === 'function' && 'mockReset' in fn) {
+ fn.mockReset()
+ }
+ })
+ }
+ }
+
+ return utilsMock
+}
+
+/**
+ * Mock for electron-log
+ */
+export function createLogMock() {
+ return {
+ info: vi.fn(),
+ error: vi.fn(),
+ warn: vi.fn(),
+ debug: vi.fn(),
+ }
+}
+
+/**
+ * Complete environment setup for testing
+ * Note: vi.mock calls should be done at the top level of test files, not here
+ */
+export function setupMockEnvironment() {
+ const fsMock = createFileSystemMock()
+ const processMock = createProcessMock()
+ const appMock = createElectronAppMock()
+ const osMock = createOsMock()
+ const pathMock = createPathMock()
+ const processUtilsMock = createProcessUtilsMock()
+ const logMock = createLogMock()
+
+ // Set up the shared state
+ processMock.setupSpawnMock(fsMock.mockState)
+ appMock.setup(fsMock.mockState)
+ osMock.setup(fsMock.mockState)
+ processUtilsMock.setup(fsMock.mockState)
+
+ return {
+ fsMock,
+ processMock,
+ appMock,
+ osMock,
+ pathMock,
+ processUtilsMock,
+ logMock,
+ mockState: fsMock.mockState,
+
+ // Utility functions for test scenarios
+ scenarios: {
+ freshInstall: () => {
+ fsMock.mockState.filesystem.venvExists = false
+ fsMock.mockState.filesystem.versionFileExists = false
+ fsMock.mockState.filesystem.installedLockExists = false
+ fsMock.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
+ fsMock.mockState.processes.uvAvailable = false
+ fsMock.mockState.processes.bunAvailable = false
+ },
+
+ versionUpdate: (oldVersion: string, newVersion: string) => {
+ fsMock.mockState.filesystem.versionFileContent = oldVersion
+ fsMock.mockState.app.currentVersion = newVersion
+ appMock.getVersion.mockReturnValue(newVersion)
+ },
+
+ venvRemoved: () => {
+ fsMock.mockState.filesystem.venvExists = false
+ fsMock.mockState.filesystem.installedLockExists = false
+ },
+
+ networkIssues: () => {
+ fsMock.mockState.network.canConnectToDefault = false
+ fsMock.mockState.network.canConnectToMirror = true
+ },
+
+ completeFailure: () => {
+ fsMock.mockState.network.canConnectToDefault = false
+ fsMock.mockState.network.canConnectToMirror = false
+ fsMock.mockState.processes.uvAvailable = false
+ fsMock.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
+
+ // Note: installCommandTool is defined in the install-deps module,
+ // not in process utils, so it should be mocked in the test itself
+ },
+
+ uvicornStartupInstall: () => {
+ fsMock.mockState.processes.uvicornRunning = false
+ fsMock.mockState.filesystem.installedLockExists = false
+ // Uvicorn will detect missing deps and start installation
+ },
+
+ installationInProgress: () => {
+ fsMock.mockState.filesystem.installingLockExists = true
+ fsMock.mockState.processes.installationInProgress = true
+ },
+
+ // New scenarios for process.ts testing
+ packagedApp: () => {
+ fsMock.mockState.app.isPackaged = true
+ appMock.isPackaged = true
+ },
+
+ multipleOldVenvs: (currentVersion: string) => {
+ fsMock.mockState.filesystem.oldVenvsExist = [
+ 'backend-0.9.0',
+ 'backend-0.9.5',
+ 'backend-1.0.1-beta',
+ `backend-${currentVersion}` // This should not be cleaned up
+ ]
+ },
+
+ macOSEnvironment: () => {
+ fsMock.mockState.system.platform = 'darwin'
+ Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true })
+ },
+
+ linuxEnvironment: () => {
+ fsMock.mockState.system.platform = 'linux'
+ Object.defineProperty(process, 'platform', { value: 'linux', configurable: true })
+ },
+
+ missingEigentDirectories: () => {
+ fsMock.mockState.filesystem.eigentDirExists = false
+ fsMock.mockState.filesystem.eigentBinDirExists = false
+ fsMock.mockState.filesystem.eigentCacheDirExists = false
+ fsMock.mockState.filesystem.eigentVenvsDirExists = false
+ fsMock.mockState.filesystem.eigentRuntimeDirExists = false
+ }
+ },
+
+ reset: () => {
+ fsMock.reset()
+ processMock.reset()
+ appMock.reset()
+ osMock.reset()
+ processUtilsMock.reset()
+
+ // Reset process.platform to original
+ Object.defineProperty(process, 'platform', {
+ value: 'win32',
+ configurable: true
+ })
+ }
+ }
+}
+
+/**
+ * Factory functions for creating mocks that can be used in vi.mock calls
+ * These should be called at the top level of test files
+ */
+export function createMockFactories() {
+ return {
+ fs: () => createFileSystemMock(),
+ childProcess: () => createProcessMock(),
+ os: () => ({ default: createOsMock() }),
+ path: () => ({ default: createPathMock() }),
+ electron: () => ({
+ app: createElectronAppMock(),
+ BrowserWindow: vi.fn()
+ }),
+ electronLog: () => ({ default: createLogMock() }),
+ processUtils: () => createProcessUtilsMock()
+ }
+}
+
+/**
+ * Test utility to wait for async state changes
+ */
+export function waitForStateChange(
+ stateGetter: () => T,
+ expectedValue: T,
+ timeout: number = 1000
+): Promise {
+ return new Promise((resolve, reject) => {
+ const startTime = Date.now()
+
+ const check = () => {
+ if (stateGetter() === expectedValue) {
+ resolve()
+ } else if (Date.now() - startTime > timeout) {
+ reject(new Error(`Timeout waiting for state change. Expected: ${expectedValue}, got: ${stateGetter()}`))
+ } else {
+ setTimeout(check, 10)
+ }
+ }
+
+ check()
+ })
+}
\ No newline at end of file
diff --git a/test/mocks/testUtils.ts b/test/mocks/testUtils.ts
new file mode 100644
index 000000000..d6282d8b9
--- /dev/null
+++ b/test/mocks/testUtils.ts
@@ -0,0 +1,282 @@
+import { setupElectronMocks, TestScenarios, type MockedElectronAPI } from './electronMocks'
+import { setupMockEnvironment } from './environmentMocks'
+
+/**
+ * Complete test setup utility that combines all mocks and provides
+ * easy-to-use functions for testing installation flows
+ */
+export function createTestEnvironment() {
+ const { electronAPI, ipcRenderer } = setupElectronMocks()
+ const mockEnv = setupMockEnvironment()
+
+ return {
+ electronAPI,
+ ipcRenderer,
+ mockEnv,
+
+ // Quick scenario setups
+ scenarios: {
+ /**
+ * Fresh installation - no .venv, no version file, tools not installed
+ */
+ freshInstall: () => {
+ TestScenarios.freshInstall(electronAPI)
+ mockEnv.scenarios.freshInstall()
+ },
+
+ /**
+ * Version update - version file exists but version changed
+ */
+ versionUpdate: (oldVersion: string = '0.9.0', newVersion: string = '1.0.0') => {
+ TestScenarios.versionUpdate(electronAPI)
+ mockEnv.scenarios.versionUpdate(oldVersion, newVersion)
+ },
+
+ /**
+ * .venv removed - version file exists but .venv is missing
+ */
+ venvRemoved: () => {
+ TestScenarios.venvRemoved(electronAPI)
+ mockEnv.scenarios.venvRemoved()
+ },
+
+ /**
+ * Installation in progress - when user opens app during installation
+ */
+ installationInProgress: () => {
+ TestScenarios.installationInProgress(electronAPI)
+ mockEnv.scenarios.installationInProgress()
+ },
+
+ /**
+ * Installation error - installation fails
+ */
+ installationError: () => {
+ TestScenarios.installationError(electronAPI)
+ mockEnv.scenarios.completeFailure()
+ },
+
+ /**
+ * Uvicorn startup with dependency installation
+ */
+ uvicornDepsInstall: () => {
+ TestScenarios.uvicornDepsInstall(electronAPI)
+ mockEnv.scenarios.uvicornStartupInstall()
+ },
+
+ /**
+ * Network issues - default mirror fails, backup succeeds
+ */
+ networkIssues: () => {
+ TestScenarios.allGood(electronAPI)
+ mockEnv.scenarios.networkIssues()
+ },
+
+ /**
+ * All good - no installation needed
+ */
+ allGood: () => {
+ TestScenarios.allGood(electronAPI)
+ // Use default mockEnv state (all good)
+ }
+ },
+
+ // Simulation utilities
+ simulate: {
+ /**
+ * Simulate a complete successful installation flow
+ */
+ successfulInstallation: async (delay: number = 100) => {
+ electronAPI.simulateInstallationStart()
+
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
+ }, delay)
+
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stdout', 'Downloading packages...')
+ }, delay * 2)
+
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stdout', 'Installing packages...')
+ }, delay * 3)
+
+ setTimeout(() => {
+ electronAPI.simulateInstallationComplete(true)
+ }, delay * 4)
+ },
+
+ /**
+ * Simulate a failed installation flow
+ */
+ failedInstallation: async (delay: number = 100, errorMessage: string = 'Installation failed') => {
+ electronAPI.simulateInstallationStart()
+
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
+ }, delay)
+
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stderr', `Error: ${errorMessage}`)
+ }, delay * 2)
+
+ setTimeout(() => {
+ electronAPI.simulateInstallationComplete(false, errorMessage)
+ }, delay * 3)
+ },
+
+ /**
+ * Simulate uvicorn startup that detects missing dependencies
+ */
+ uvicornWithDeps: async (delay: number = 100) => {
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stdout', 'Uvicorn detected missing dependencies')
+ }, delay)
+
+ setTimeout(() => {
+ electronAPI.simulateInstallationStart()
+ }, delay * 2)
+
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
+ }, delay * 3)
+
+ setTimeout(() => {
+ electronAPI.simulateInstallationLog('stdout', 'Uvicorn running on http://127.0.0.1:8000')
+ electronAPI.simulateInstallationComplete(true)
+ }, delay * 4)
+ }
+ },
+
+ // State inspection utilities
+ inspect: {
+ /**
+ * Get current installation state summary
+ */
+ getInstallationState: () => ({
+ electronState: electronAPI.mockState,
+ envState: mockEnv.mockState,
+ isInstalling: electronAPI.mockState.isInstalling || mockEnv.mockState.processes.installationInProgress,
+ hasLockFiles: mockEnv.mockState.filesystem.installingLockExists || mockEnv.mockState.filesystem.installedLockExists,
+ toolsAvailable: mockEnv.mockState.processes.uvAvailable && mockEnv.mockState.processes.bunAvailable,
+ venvExists: electronAPI.mockState.venvExists && mockEnv.mockState.filesystem.venvExists,
+ }),
+
+ /**
+ * Check if environment is in expected state for a scenario
+ */
+ verifyScenario: (scenarioName: string) => {
+ const state = mockEnv.mockState
+ const electronState = electronAPI.mockState
+
+ switch (scenarioName) {
+ case 'freshInstall':
+ return !state.filesystem.venvExists &&
+ !state.filesystem.versionFileExists &&
+ !electronState.toolInstalled
+
+ case 'versionUpdate':
+ return state.filesystem.versionFileExists &&
+ state.app.currentVersion !== state.filesystem.versionFileContent
+
+ case 'venvRemoved':
+ return !state.filesystem.venvExists &&
+ state.filesystem.versionFileExists
+
+ case 'installationInProgress':
+ return state.filesystem.installingLockExists ||
+ electronState.isInstalling
+
+ default:
+ return false
+ }
+ }
+ },
+
+ // Reset everything
+ reset: () => {
+ electronAPI.reset()
+ mockEnv.reset()
+ }
+ }
+}
+
+/**
+ * Helper function to wait for installation state changes
+ */
+export async function waitForInstallationState(
+ getState: () => any,
+ expectedState: string,
+ timeout: number = 1000
+): Promise {
+ return new Promise((resolve, reject) => {
+ const startTime = Date.now()
+
+ const check = () => {
+ if (getState().state === expectedState) {
+ resolve()
+ } else if (Date.now() - startTime > timeout) {
+ reject(new Error(`Timeout waiting for state ${expectedState}, current: ${getState().state}`))
+ } else {
+ setTimeout(check, 10)
+ }
+ }
+
+ check()
+ })
+}
+
+/**
+ * Helper function to wait for multiple logs
+ */
+export async function waitForLogs(
+ getLogs: () => any[],
+ expectedCount: number,
+ timeout: number = 1000
+): Promise {
+ return new Promise((resolve, reject) => {
+ const startTime = Date.now()
+
+ const check = () => {
+ if (getLogs().length >= expectedCount) {
+ resolve()
+ } else if (Date.now() - startTime > timeout) {
+ reject(new Error(`Timeout waiting for ${expectedCount} logs, got: ${getLogs().length}`))
+ } else {
+ setTimeout(check, 10)
+ }
+ }
+
+ check()
+ })
+}
+
+/**
+ * Example usage in tests:
+ *
+ * ```typescript
+ * import { createTestEnvironment, waitForInstallationState } from '../mocks/testUtils'
+ *
+ * describe('Installation Flow', () => {
+ * let testEnv: ReturnType
+ *
+ * beforeEach(() => {
+ * testEnv = createTestEnvironment()
+ * })
+ *
+ * it('should handle fresh installation', async () => {
+ * testEnv.scenarios.freshInstall()
+ *
+ * // Verify scenario setup
+ * expect(testEnv.inspect.verifyScenario('freshInstall')).toBe(true)
+ *
+ * // Simulate installation
+ * await testEnv.simulate.successfulInstallation()
+ *
+ * // Verify result
+ * const state = testEnv.inspect.getInstallationState()
+ * expect(state.isInstalling).toBe(false)
+ * })
+ * })
+ * ```
+ */
\ No newline at end of file
diff --git a/test/setup.ts b/test/setup.ts
index 907391e69..54d7c529a 100644
--- a/test/setup.ts
+++ b/test/setup.ts
@@ -2,11 +2,48 @@
import { vi } from 'vitest'
import '@testing-library/jest-dom'
+// Mock react-i18next
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => {
+ // Map translation keys to English text
+ const translations: Record = {
+ 'chat.welcome-to-eigent': 'Welcome to Eigent',
+ 'chat.how-can-i-help-you': 'How can I help you today?',
+ 'chat.palm-springs-tennis-trip-planner': 'Palm Springs Tennis Trip Planner',
+ 'chat.bank-transfer-csv-analysis-and-visualization': 'Bank Transfer CSV Analysis and Visualization',
+ 'chat.find-duplicate-files-in-downloads-folder': 'Find Duplicate Files in Downloads Folder',
+ 'setting.search-mcp': 'Search MCPs',
+ 'chat.by-messaging-eigent': 'By messaging Eigent, you agree to our',
+ 'chat.terms-of-use': 'Terms of Use',
+ 'chat.and': 'and',
+ 'chat.privacy-policy': 'Privacy Policy',
+ 'chat.palm-springs-tennis-trip-planner-message': 'Plan a tennis trip to Palm Springs',
+ 'chat.bank-transfer-csv-analysis-and-visualization-message': 'Analyze and visualize bank transfer CSV',
+ 'chat.find-duplicate-files-in-downloads-folder-message': 'Find duplicate files in Downloads folder',
+ 'chat.no-reply-received-task-continue': 'No reply received, task will continue',
+ }
+ return translations[key] || key
+ },
+ i18n: {
+ language: 'en',
+ changeLanguage: vi.fn(),
+ },
+ }),
+}))
+
// Mock Electron APIs if needed
global.electronAPI = {
// Add mock implementations for electron preload APIs
}
+// Mock ipcRenderer
+global.ipcRenderer = {
+ invoke: vi.fn(),
+ on: vi.fn(),
+ removeAllListeners: vi.fn(),
+}
+
// Mock environment variables
process.env.NODE_ENV = 'test'
@@ -22,6 +59,13 @@ global.waitFor = async (callback: () => boolean, timeout = 5000) => {
throw new Error(`Timeout waiting for condition after ${timeout}ms`)
}
+// Add type declarations for globals
+declare global {
+ var electronAPI: any
+ var ipcRenderer: any
+ var waitFor: (callback: () => boolean, timeout?: number) => Promise
+}
+
// Setup DOM environment
Object.defineProperty(window, 'matchMedia', {
writable: true,
diff --git a/test/unit/components/ChatBox.test.tsx b/test/unit/components/ChatBox.test.tsx
index ca13746b1..c19bc5f0e 100644
--- a/test/unit/components/ChatBox.test.tsx
+++ b/test/unit/components/ChatBox.test.tsx
@@ -737,11 +737,21 @@ describe('ChatBox Component', () => {
})
it('should handle privacy fetch errors', async () => {
+ // Mock console.error to suppress expected error logs
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+
// Mock the fetch to reject properly for testing error handling
mockProxyFetchGet.mockRejectedValue(new Error('Privacy fetch failed'))
// Rendering should not throw even with fetch error
expect(() => renderChatBox()).not.toThrow()
+
+ // Wait for the promise to settle
+ await waitFor(() => {
+ expect(consoleErrorSpy).toHaveBeenCalled()
+ })
+
+ consoleErrorSpy.mockRestore()
})
})
})
diff --git a/test/unit/electron/install-deps.test.ts b/test/unit/electron/install-deps.test.ts
new file mode 100644
index 000000000..b1ee8adc0
--- /dev/null
+++ b/test/unit/electron/install-deps.test.ts
@@ -0,0 +1,609 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
+import { setupMockEnvironment } from '../../mocks/environmentMocks'
+
+// Set up global mock environment before any imports
+const globalMockEnv = setupMockEnvironment()
+
+// Mock all dependencies at the top level
+vi.mock('node:fs', () => ({
+ ...globalMockEnv.fsMock,
+ default: globalMockEnv.fsMock
+}))
+vi.mock('node:path', () => ({
+ ...globalMockEnv.pathMock,
+ default: globalMockEnv.pathMock
+}))
+vi.mock('child_process', () => ({
+ ...globalMockEnv.processMock,
+ default: globalMockEnv.processMock
+}))
+vi.mock('electron-log', () => ({ default: globalMockEnv.logMock }))
+vi.mock('electron', () => ({
+ app: globalMockEnv.appMock,
+ BrowserWindow: vi.fn()
+}))
+vi.mock('../../../electron/main/utils/process', () => globalMockEnv.processUtilsMock)
+vi.mock('../../../electron/main/init', () => ({
+ getMainWindow: vi.fn().mockReturnValue({
+ webContents: { send: vi.fn() },
+ isDestroyed: vi.fn().mockReturnValue(false)
+ })
+}))
+vi.mock('../../../electron/main/utils/safeWebContentsSend', () => ({
+ safeMainWindowSend: vi.fn().mockReturnValue(true)
+}))
+
+// Import the module under test after mocking
+let installDeps: any
+
+describe('Install Deps Module', () => {
+ let mockEnv: ReturnType
+
+ beforeEach(async () => {
+ // Reset the mock environment state for each test
+ mockEnv = globalMockEnv
+ mockEnv.reset()
+
+ // Set up the shared state
+ mockEnv.processMock.setupSpawnMock(mockEnv.mockState)
+ mockEnv.appMock.setup(mockEnv.mockState)
+ mockEnv.osMock.setup(mockEnv.mockState)
+ mockEnv.processUtilsMock.setup(mockEnv.mockState)
+
+ // Import the module under test fresh for each test
+ installDeps = await import('../../../electron/main/install-deps')
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ mockEnv.reset()
+ })
+
+ describe('checkAndInstallDepsOnUpdate', () => {
+ it('should skip installation when version has not changed and tools are installed', async () => {
+ // Set up scenario where version is the same and tools exist
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
+
+ const mockWin = {
+ webContents: { send: vi.fn() },
+ isDestroyed: vi.fn().mockReturnValue(false)
+ }
+
+ const result = await installDeps.checkAndInstallDepsOnUpdate({
+ win: mockWin,
+ forceInstall: false
+ })
+
+ expect(result.success).toBe(true)
+ expect(result.message).toContain('Version not changed')
+ expect(mockEnv.fsMock.writeFileSync).not.toHaveBeenCalledWith(
+ expect.stringContaining('version.txt'),
+ expect.any(String)
+ )
+ })
+
+ it('should install dependencies when version file does not exist', async () => {
+ // Set up fresh installation scenario
+ mockEnv.scenarios.freshInstall()
+
+ const mockWin = {
+ webContents: { send: vi.fn() },
+ isDestroyed: vi.fn().mockReturnValue(false)
+ }
+
+ const result = await installDeps.checkAndInstallDepsOnUpdate({
+ win: mockWin,
+ forceInstall: false
+ })
+ console.log(result);
+
+ expect(result.success).toBe(true)
+ expect(result.message).toContain('Dependencies installed successfully')
+ expect(mockWin.webContents.send).toHaveBeenCalledWith(
+ 'update-notification',
+ expect.objectContaining({
+ type: 'version-update',
+ reason: 'version file not exist'
+ })
+ )
+ })
+
+ it('should install dependencies when version has changed', async () => {
+ // Set up version update scenario
+ mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
+
+ const mockWin = {
+ webContents: { send: vi.fn() },
+ isDestroyed: vi.fn().mockReturnValue(false)
+ }
+
+ const result = await installDeps.checkAndInstallDepsOnUpdate({
+ win: mockWin,
+ forceInstall: false
+ })
+
+ expect(result.success).toBe(true)
+ expect(result.message).toContain('Dependencies installed successfully')
+ expect(mockWin.webContents.send).toHaveBeenCalledWith(
+ 'update-notification',
+ expect.objectContaining({
+ type: 'version-update',
+ currentVersion: '1.0.0',
+ previousVersion: '0.9.0',
+ reason: 'version not match'
+ })
+ )
+ })
+
+ it('should install when command tools are missing', async () => {
+ // Same version but tools missing
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': true }
+
+ const mockWin = {
+ webContents: { send: vi.fn() },
+ isDestroyed: vi.fn().mockReturnValue(false)
+ }
+
+ const result = await installDeps.checkAndInstallDepsOnUpdate({
+ win: mockWin,
+ forceInstall: false
+ })
+
+ expect(result.success).toBe(true)
+ expect(result.message).toContain('Dependencies installed successfully')
+ })
+
+ it('should force install when forceInstall is true', async () => {
+ // Set up scenario where normally no installation would be needed
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
+
+ const mockWin = {
+ webContents: { send: vi.fn() },
+ isDestroyed: vi.fn().mockReturnValue(false)
+ }
+
+ const result = await installDeps.checkAndInstallDepsOnUpdate({
+ win: mockWin,
+ forceInstall: true
+ })
+
+ expect(result.success).toBe(true)
+ expect(result.message).toContain('Dependencies installed successfully')
+ })
+
+ it('should handle installation failure', async () => {
+ // Set up failure scenario
+ mockEnv.scenarios.completeFailure()
+
+ const mockWin = {
+ webContents: { send: vi.fn() },
+ isDestroyed: vi.fn().mockReturnValue(false)
+ }
+
+ const result = await installDeps.checkAndInstallDepsOnUpdate({
+ win: mockWin,
+ forceInstall: true
+ })
+
+ expect(result.success).toBe(false)
+ expect(result.message).toContain('Install dependencies failed')
+ })
+
+ it('should handle window being destroyed', async () => {
+ const mockWin = {
+ webContents: { send: vi.fn() },
+ isDestroyed: vi.fn().mockReturnValue(true)
+ }
+
+ mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
+
+ const result = await installDeps.checkAndInstallDepsOnUpdate({
+ win: mockWin,
+ forceInstall: false
+ })
+
+ // Should still complete successfully
+ expect(result.success).toBe(true)
+ // Should not try to send notifications to destroyed window
+ expect(mockWin.webContents.send).not.toHaveBeenCalled()
+ })
+
+ it('should handle null window gracefully', async () => {
+ mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
+
+ const result = await installDeps.checkAndInstallDepsOnUpdate({
+ win: null,
+ forceInstall: false
+ })
+
+ expect(result.success).toBe(true)
+ // Should not crash when window is null
+ })
+ })
+
+ describe('installCommandTool', () => {
+ it('should install uv and bun when not available', async () => {
+ // Set up scenario where tools are not available initially
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
+
+ // Simulate successful installation
+ let uvCallCount = 0
+ let bunCallCount = 0
+ mockEnv.processUtilsMock.isBinaryExists.mockImplementation(async (name: string) => {
+ if (name === 'uv') {
+ uvCallCount++
+ return uvCallCount > 1 // False first time, true after "installation"
+ }
+ if (name === 'bun') {
+ bunCallCount++
+ return bunCallCount > 1 // False first time, true after "installation"
+ }
+ return false
+ })
+
+ const result = await installDeps.installCommandTool()
+
+ expect(result.success).toBe(true)
+ expect(result.message).toContain('Command tools installed successfully')
+ expect(mockEnv.processUtilsMock.runInstallScript).toHaveBeenCalledTimes(2) // uv and bun
+ })
+
+ it('should skip installation when tools are already available', async () => {
+ // Tools are available by default in mockState
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
+
+ const result = await installDeps.installCommandTool()
+
+ expect(result.success).toBe(true)
+ expect(result.message).toContain('Command tools installed successfully')
+ expect(mockEnv.processUtilsMock.runInstallScript).not.toHaveBeenCalled()
+ })
+
+ it('should handle uv installation failure', async () => {
+ // Mock uv installation failure
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': true }
+
+ // uv remains unavailable even after installation attempt
+ mockEnv.processUtilsMock.isBinaryExists.mockImplementation(async (name: string) => {
+ if (name === 'uv') return false // Always fails
+ if (name === 'bun') return true
+ return false
+ })
+
+ const result = await installDeps.installCommandTool()
+
+ expect(result.success).toBe(false)
+ expect(result.message).toContain('uv installation failed')
+ })
+
+ it('should handle bun installation failure', async () => {
+ // Mock bun installation failure
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': false }
+
+ // bun remains unavailable even after installation attempt
+ mockEnv.processUtilsMock.isBinaryExists.mockImplementation(async (name: string) => {
+ if (name === 'uv') return true
+ if (name === 'bun') return false // Always fails
+ return false
+ })
+
+ const result = await installDeps.installCommandTool()
+
+ expect(result.success).toBe(false)
+ expect(result.message).toContain('bun installation failed')
+ })
+ })
+})
+
+// describe('getInstallationStatus', () => {
+// it('should return correct status when installation is in progress', async () => {
+// mockEnv.scenarios.installationInProgress()
+
+// const status = await installDeps.getInstallationStatus()
+
+// expect(status.isInstalling).toBe(true)
+// expect(status.hasLockFile).toBe(true)
+// expect(status.installedExists).toBe(false)
+// })
+
+// it('should return correct status when installation is completed', async () => {
+// // Default state has installation completed
+// mockEnv.mockState.filesystem.installingLockExists = false
+// mockEnv.mockState.filesystem.installedLockExists = true
+
+// const status = await installDeps.getInstallationStatus()
+
+// expect(status.isInstalling).toBe(false)
+// expect(status.hasLockFile).toBe(true)
+// expect(status.installedExists).toBe(true)
+// })
+
+// it('should return correct status when no installation has occurred', async () => {
+// mockEnv.mockState.filesystem.installingLockExists = false
+// mockEnv.mockState.filesystem.installedLockExists = false
+
+// const status = await installDeps.getInstallationStatus()
+
+// expect(status.isInstalling).toBe(false)
+// expect(status.hasLockFile).toBe(false)
+// expect(status.installedExists).toBe(false)
+// })
+
+// it('should handle file system errors gracefully', async () => {
+// // Mock fs.existsSync to throw an error
+// mockEnv.fsMock.existsSync.mockImplementation(() => {
+// throw new Error('File system error')
+// })
+
+// const status = await installDeps.getInstallationStatus()
+
+// expect(status.isInstalling).toBe(false)
+// expect(status.hasLockFile).toBe(false)
+// expect(status.installedExists).toBe(false)
+// })
+// })
+
+// describe('installDependencies', () => {
+// it('should successfully install dependencies with default settings', async () => {
+// // Set up successful installation scenario
+// mockEnv.mockState.processes.uvAvailable = true
+// mockEnv.mockState.network.canConnectToDefault = true
+
+// const result = await installDeps.installDependencies('1.0.0')
+
+// expect(result.success).toBe(true)
+// expect(result.message).toContain('Dependencies installed successfully')
+// expect(mockEnv.fsMock.writeFileSync).toHaveBeenCalledWith(
+// expect.stringContaining('uv_installed.lock'),
+// ''
+// )
+// })
+
+// it('should fall back to mirror when default fails', async () => {
+// // Set up network issues scenario - first install fails, mirror succeeds
+// mockEnv.scenarios.networkIssues()
+
+// const result = await installDeps.installDependencies('1.0.0')
+
+// expect(result.success).toBe(true)
+// expect(result.message).toContain('Dependencies installed successfully with mirror')
+// })
+
+// it('should fail when both default and mirror fail', async () => {
+// mockEnv.scenarios.completeFailure()
+
+// const result = await installDeps.installDependencies('1.0.0')
+
+// expect(result.success).toBe(false)
+// expect(result.message).toContain('Both default and mirror install failed')
+// })
+
+// it('should handle command tool installation failure', async () => {
+// // Set up scenario where command tool installation fails
+// mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
+
+// // Mock tools to remain unavailable
+// mockEnv.processUtilsMock.isBinaryExists.mockResolvedValue(false)
+
+// const result = await installDeps.installDependencies('1.0.0')
+
+// expect(result.success).toBe(false)
+// expect(result.message).toContain('Command tool installation failed')
+// })
+
+// it('should create and clean up lock files correctly', async () => {
+// mockEnv.mockState.processes.uvAvailable = true
+// mockEnv.mockState.network.canConnectToDefault = true
+
+// await installDeps.installDependencies('1.0.0')
+
+// // Verify that installation lock is created and then cleaned up
+// expect(mockEnv.fsMock.writeFileSync).toHaveBeenCalledWith(
+// expect.stringContaining('uv_installing.lock'),
+// ''
+// )
+// expect(mockEnv.fsMock.unlinkSync).toHaveBeenCalledWith(
+// expect.stringContaining('uv_installing.lock')
+// )
+// expect(mockEnv.fsMock.writeFileSync).toHaveBeenCalledWith(
+// expect.stringContaining('uv_installed.lock'),
+// ''
+// )
+// })
+
+// it('should clean up old virtual environments after successful installation', async () => {
+// mockEnv.scenarios.multipleOldVenvs('1.0.0')
+// mockEnv.mockState.processes.uvAvailable = true
+// mockEnv.mockState.network.canConnectToDefault = true
+
+// await installDeps.installDependencies('1.0.0')
+
+// expect(mockEnv.processUtilsMock.cleanupOldVenvs).toHaveBeenCalledWith('1.0.0')
+// })
+// })
+
+// describe('detectInstallationLogs', () => {
+// beforeEach(() => {
+// // Reset the module-level state variables
+// vi.resetModules()
+// })
+
+// it('should detect UV dependency installation patterns', () => {
+// const installationPatterns = [
+// 'Resolved 10 packages in 1.2s',
+// 'Downloaded package xyz',
+// 'Installing numpy==1.21.0',
+// 'Built wheel for package',
+// 'Prepared virtual environment',
+// 'Syncing dependencies',
+// 'Creating virtualenv at .venv',
+// 'Updating package index',
+// 'Audited 15 packages'
+// ]
+
+// installationPatterns.forEach(pattern => {
+// // The function has side effects, so we can't easily test return values
+// // Instead, we test that it doesn't throw and processes the input
+// expect(() => installDeps.detectInstallationLogs(pattern)).not.toThrow()
+// })
+// })
+
+// it('should handle uvicorn startup messages', () => {
+// const uvicornMessages = [
+// 'Uvicorn running on http://127.0.0.1:8000',
+// 'Application startup complete',
+// 'Server started successfully'
+// ]
+
+// uvicornMessages.forEach(message => {
+// expect(() => installDeps.detectInstallationLogs(message)).not.toThrow()
+// })
+// })
+
+// it('should handle installation failure messages', () => {
+// const failureMessages = [
+// '× No solution found when resolving dependencies',
+// 'failed to resolve dependencies',
+// 'installation failed'
+// ]
+
+// failureMessages.forEach(message => {
+// expect(() => installDeps.detectInstallationLogs(message)).not.toThrow()
+// })
+// })
+// })
+
+// describe('Error Handling and Edge Cases', () => {
+// it('should handle file system permission errors gracefully', async () => {
+// // Mock filesystem error
+// mockEnv.fsMock.writeFileSync.mockImplementation((path: string) => {
+// if (path.includes('version.txt')) {
+// throw new Error('Permission denied')
+// }
+// })
+
+// const mockWin = {
+// webContents: { send: vi.fn() },
+// isDestroyed: vi.fn().mockReturnValue(false)
+// }
+
+// // The function should handle errors gracefully
+// const result = await installDeps.checkAndInstallDepsOnUpdate({
+// win: mockWin,
+// forceInstall: true
+// })
+
+// // Should still return a result, even if there are file system errors
+// expect(result).toBeDefined()
+// expect(typeof result.success).toBe('boolean')
+// expect(typeof result.message).toBe('string')
+// })
+
+// it('should handle timezone-based mirror selection for China', async () => {
+// // Mock Intl.DateTimeFormat for China timezone
+// const originalDateTimeFormat = global.Intl.DateTimeFormat
+// const mockDateTimeFormat = vi.fn().mockImplementation(() => ({
+// resolvedOptions: () => ({ timeZone: 'Asia/Shanghai' })
+// })) as any
+// global.Intl.DateTimeFormat = mockDateTimeFormat
+
+// try {
+// // Set up scenario where default fails but mirror succeeds
+// mockEnv.scenarios.networkIssues()
+
+// const result = await installDeps.installDependencies('1.0.0')
+
+// expect(result.success).toBe(true)
+// expect(result.message).toContain('Dependencies installed successfully with mirror')
+// } finally {
+// // Restore original
+// global.Intl.DateTimeFormat = originalDateTimeFormat
+// }
+// })
+
+// it('should handle invalid version strings', async () => {
+// const result = await installDeps.installDependencies('')
+
+// // Should handle empty version string gracefully
+// expect(result).toBeDefined()
+// expect(typeof result.success).toBe('boolean')
+// })
+
+// it('should handle missing backend directory', async () => {
+// mockEnv.mockState.filesystem.backendPathExists = false
+
+// const result = await installDeps.installDependencies('1.0.0')
+
+// // Should create the directory and continue
+// expect(mockEnv.fsMock.mkdirSync).toHaveBeenCalledWith(
+// expect.stringContaining('backend'),
+// { recursive: true }
+// )
+// expect(result).toBeDefined()
+// })
+// })
+
+// describe('Integration Tests', () => {
+// it('should handle complete fresh installation workflow', async () => {
+// // Set up completely fresh system
+// mockEnv.scenarios.freshInstall()
+
+// const mockWin = {
+// webContents: { send: vi.fn() },
+// isDestroyed: vi.fn().mockReturnValue(false)
+// }
+
+// const result = await installDeps.checkAndInstallDepsOnUpdate({
+// win: mockWin,
+// forceInstall: false
+// })
+
+// expect(result.success).toBe(true)
+// expect(mockWin.webContents.send).toHaveBeenCalledWith(
+// 'update-notification',
+// expect.objectContaining({
+// type: 'version-update',
+// reason: 'version file not exist'
+// })
+// )
+// })
+
+// it('should handle version update with missing tools', async () => {
+// // Version file exists but tools are missing
+// mockEnv.mockState.filesystem.versionFileExists = true
+// mockEnv.mockState.filesystem.versionFileContent = '0.9.0'
+// mockEnv.mockState.app.currentVersion = '1.0.0'
+// mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
+
+// const mockWin = {
+// webContents: { send: vi.fn() },
+// isDestroyed: vi.fn().mockReturnValue(false)
+// }
+
+// const result = await installDeps.checkAndInstallDepsOnUpdate({
+// win: mockWin,
+// forceInstall: false
+// })
+
+// expect(result.success).toBe(true)
+// expect(mockWin.webContents.send).toHaveBeenCalledWith(
+// 'update-notification',
+// expect.objectContaining({
+// type: 'version-update',
+// currentVersion: '1.0.0',
+// previousVersion: '0.9.0',
+// reason: 'version not match'
+// })
+// )
+// })
+// })
+// })
\ No newline at end of file
diff --git a/test/unit/electron/main/domReadyHandlers.test.ts b/test/unit/electron/main/domReadyHandlers.test.ts
new file mode 100644
index 000000000..816bcd010
--- /dev/null
+++ b/test/unit/electron/main/domReadyHandlers.test.ts
@@ -0,0 +1,539 @@
+/**
+ * Tests for DOM ready event handlers in createWindow function
+ * These handlers manage localStorage injection for installation states
+ */
+
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
+import { setupMockEnvironment } from '../../../mocks/environmentMocks'
+
+describe('createWindow - DOM Ready Event Handlers', () => {
+ let mockEnv: ReturnType
+ let mockWebContents: any
+ let mockWindow: any
+
+ beforeEach(() => {
+ mockEnv = setupMockEnvironment()
+
+ // Mock webContents and window
+ mockWebContents = {
+ on: vi.fn(),
+ once: vi.fn(),
+ executeJavaScript: vi.fn(),
+ send: vi.fn(),
+ loadURL: vi.fn(),
+ loadFile: vi.fn(),
+ openDevTools: vi.fn()
+ }
+
+ mockWindow = {
+ webContents: mockWebContents,
+ reload: vi.fn()
+ }
+
+ // Reset all mocks
+ vi.clearAllMocks()
+ })
+
+ afterEach(() => {
+ mockEnv.reset()
+ })
+
+ describe('Fresh Installation - Carousel State Injection', () => {
+ it('should inject fresh auth-storage with carousel state', () => {
+ // Simulate fresh installation scenario
+ const needsInstallation = true
+
+ // Set up DOM ready handler like createWindow does
+ if (needsInstallation) {
+ mockWebContents.on('dom-ready', () => {
+ const injectionScript = `
+ (function() {
+ try {
+ const newAuthStorage = {
+ state: {
+ token: null,
+ username: null,
+ email: null,
+ user_id: null,
+ appearance: 'light',
+ language: 'system',
+ isFirstLaunch: true,
+ modelType: 'cloud',
+ cloud_model_type: 'gpt-4.1',
+ initState: 'carousel',
+ share_token: null,
+ workerListData: {}
+ },
+ version: 0
+ };
+ localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
+ console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
+ } catch (e) {
+ console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e);
+ }
+ })();
+ `
+ mockWebContents.executeJavaScript(injectionScript)
+ })
+ }
+
+ // Trigger DOM ready event
+ const domReadyCallback = mockWebContents.on.mock.calls.find(
+ (call: any) => call[0] === 'dom-ready'
+ )?.[1]
+
+ expect(domReadyCallback).toBeDefined()
+
+ if (domReadyCallback) {
+ domReadyCallback()
+ }
+
+ // Verify JavaScript injection was called with carousel state
+ expect(mockWebContents.executeJavaScript).toHaveBeenCalledWith(
+ expect.stringContaining('initState: \'carousel\'')
+ )
+ expect(mockWebContents.executeJavaScript).toHaveBeenCalledWith(
+ expect.stringContaining('isFirstLaunch: true')
+ )
+ })
+
+ it('should handle JavaScript injection errors gracefully', () => {
+ const needsInstallation = true
+
+ // Mock executeJavaScript to reject
+ mockWebContents.executeJavaScript.mockRejectedValue(new Error('Injection failed'))
+
+ // Set up DOM ready handler with error handling
+ if (needsInstallation) {
+ mockWebContents.on('dom-ready', () => {
+ const injectionScript = `/* injection script */`
+ mockWebContents.executeJavaScript(injectionScript).catch((err: Error) => {
+ // In real code, this is logged but doesn't throw
+ console.error('Failed to inject script:', err)
+ })
+ })
+ }
+
+ // Trigger DOM ready event
+ const domReadyCallback = mockWebContents.on.mock.calls.find(
+ (call: any) => call[0] === 'dom-ready'
+ )?.[1]
+
+ if (domReadyCallback) {
+ expect(() => domReadyCallback()).not.toThrow()
+ }
+ })
+
+ it('should include all required auth-storage properties', () => {
+ const needsInstallation = true
+
+ if (needsInstallation) {
+ mockWebContents.on('dom-ready', () => {
+ const injectionScript = `
+ (function() {
+ try {
+ const newAuthStorage = {
+ state: {
+ token: null,
+ username: null,
+ email: null,
+ user_id: null,
+ appearance: 'light',
+ language: 'system',
+ isFirstLaunch: true,
+ modelType: 'cloud',
+ cloud_model_type: 'gpt-4.1',
+ initState: 'carousel',
+ share_token: null,
+ workerListData: {}
+ },
+ version: 0
+ };
+ localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
+ } catch (e) {
+ console.error('Failed to create storage:', e);
+ }
+ })();
+ `
+ mockWebContents.executeJavaScript(injectionScript)
+ })
+ }
+
+ const domReadyCallback = mockWebContents.on.mock.calls.find(
+ (call: any) => call[0] === 'dom-ready'
+ )?.[1]
+
+ if (domReadyCallback) {
+ domReadyCallback()
+ }
+
+ const injectedScript = mockWebContents.executeJavaScript.mock.calls[0]?.[0]
+
+ // Verify all required properties are included
+ expect(injectedScript).toContain('token: null')
+ expect(injectedScript).toContain('username: null')
+ expect(injectedScript).toContain('email: null')
+ expect(injectedScript).toContain('user_id: null')
+ expect(injectedScript).toContain('appearance: \'light\'')
+ expect(injectedScript).toContain('language: \'system\'')
+ expect(injectedScript).toContain('isFirstLaunch: true')
+ expect(injectedScript).toContain('modelType: \'cloud\'')
+ expect(injectedScript).toContain('cloud_model_type: \'gpt-4.1\'')
+ expect(injectedScript).toContain('initState: \'carousel\'')
+ expect(injectedScript).toContain('share_token: null')
+ expect(injectedScript).toContain('workerListData: {}')
+ expect(injectedScript).toContain('version: 0')
+ })
+ })
+
+ describe('Completed Installation - Done State Management', () => {
+ it('should check and update initState to done when installation is complete', () => {
+ const needsInstallation = false
+
+ if (!needsInstallation) {
+ mockWebContents.once('dom-ready', () => {
+ const checkScript = `
+ (function() {
+ try {
+ const authStorage = localStorage.getItem('auth-storage');
+ if (authStorage) {
+ const parsed = JSON.parse(authStorage);
+ if (parsed.state && parsed.state.initState !== 'done') {
+ console.log('[ELECTRON] Updating initState from', parsed.state.initState, 'to done');
+ parsed.state.initState = 'done';
+ localStorage.setItem('auth-storage', JSON.stringify(parsed));
+ console.log('[ELECTRON] initState updated to done, reloading page...');
+ return true;
+ }
+ }
+ return false;
+ } catch (e) {
+ console.error('[ELECTRON] Failed to update initState:', e);
+ return false;
+ }
+ })();
+ `
+ mockWebContents.executeJavaScript(checkScript)
+ })
+ }
+
+ // Trigger DOM ready event
+ const domReadyCallback = mockWebContents.once.mock.calls.find(
+ (call: any) => call[0] === 'dom-ready'
+ )?.[1]
+
+ expect(domReadyCallback).toBeDefined()
+
+ if (domReadyCallback) {
+ domReadyCallback()
+ }
+
+ // Verify the check script was executed
+ expect(mockWebContents.executeJavaScript).toHaveBeenCalledWith(
+ expect.stringContaining('initState !== \'done\'')
+ )
+ expect(mockWebContents.executeJavaScript).toHaveBeenCalledWith(
+ expect.stringContaining('initState = \'done\'')
+ )
+ })
+
+ it('should trigger window reload when initState needs updating', async () => {
+ const needsInstallation = false
+
+ // Mock executeJavaScript to return true (indicating reload needed)
+ mockWebContents.executeJavaScript.mockResolvedValue(true)
+
+ if (!needsInstallation) {
+ mockWebContents.once('dom-ready', () => {
+ mockWebContents.executeJavaScript(`/* check script */`).then((needsReload: boolean) => {
+ if (needsReload) {
+ mockWindow.reload()
+ }
+ })
+ })
+ }
+
+ // Trigger DOM ready event
+ const domReadyCallback = mockWebContents.once.mock.calls.find(
+ (call: any) => call[0] === 'dom-ready'
+ )?.[1]
+
+ if (domReadyCallback) {
+ domReadyCallback()
+ }
+
+ // Wait for async operations
+ await new Promise(resolve => setTimeout(resolve, 10))
+
+ expect(mockWindow.reload).toHaveBeenCalled()
+ })
+
+ it('should not reload when initState is already done', async () => {
+ const needsInstallation = false
+
+ // Mock executeJavaScript to return false (no reload needed)
+ mockWebContents.executeJavaScript.mockResolvedValue(false)
+
+ if (!needsInstallation) {
+ mockWebContents.once('dom-ready', () => {
+ mockWebContents.executeJavaScript(`/* check script */`).then((needsReload: boolean) => {
+ if (needsReload) {
+ mockWindow.reload()
+ }
+ })
+ })
+ }
+
+ const domReadyCallback = mockWebContents.once.mock.calls.find(
+ (call: any) => call[0] === 'dom-ready'
+ )?.[1]
+
+ if (domReadyCallback) {
+ domReadyCallback()
+ }
+
+ // Wait for async operations
+ await new Promise(resolve => setTimeout(resolve, 10))
+
+ expect(mockWindow.reload).not.toHaveBeenCalled()
+ })
+
+ it('should handle localStorage parsing errors gracefully', async () => {
+ const needsInstallation = false
+
+ // Mock executeJavaScript to simulate parsing error (return false)
+ mockWebContents.executeJavaScript.mockResolvedValue(false)
+
+ if (!needsInstallation) {
+ mockWebContents.once('dom-ready', () => {
+ const checkScript = `
+ (function() {
+ try {
+ const authStorage = localStorage.getItem('auth-storage');
+ if (authStorage) {
+ const parsed = JSON.parse(authStorage); // This could throw
+ // ... rest of logic
+ }
+ return false;
+ } catch (e) {
+ console.error('[ELECTRON] Failed to update initState:', e);
+ return false; // Error case returns false
+ }
+ })();
+ `
+ mockWebContents.executeJavaScript(checkScript)
+ })
+ }
+
+ const domReadyCallback = mockWebContents.once.mock.calls.find(
+ (call: any) => call[0] === 'dom-ready'
+ )?.[1]
+
+ if (domReadyCallback) {
+ expect(() => domReadyCallback()).not.toThrow()
+ }
+
+ await new Promise(resolve => setTimeout(resolve, 10))
+
+ // Should not reload on error
+ expect(mockWindow.reload).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('Event Handler Setup Differences', () => {
+ it('should use "on" event for fresh installation (can trigger multiple times)', () => {
+ const needsInstallation = true
+
+ // Simulate the logic from createWindow
+ if (needsInstallation) {
+ mockWebContents.on('dom-ready', () => {
+ // Fresh installation handler
+ })
+ }
+
+ // Verify 'on' was used instead of 'once'
+ expect(mockWebContents.on).toHaveBeenCalledWith('dom-ready', expect.any(Function))
+ expect(mockWebContents.once).not.toHaveBeenCalled()
+ })
+
+ it('should use "once" event for completed installation (single trigger)', () => {
+ const needsInstallation = false
+
+ // Simulate the logic from createWindow
+ if (!needsInstallation) {
+ mockWebContents.once('dom-ready', () => {
+ // Completed installation handler
+ })
+ }
+
+ // Verify 'once' was used instead of 'on'
+ expect(mockWebContents.once).toHaveBeenCalledWith('dom-ready', expect.any(Function))
+ expect(mockWebContents.on).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('JavaScript Execution Content Validation', () => {
+ it('should create properly structured auth-storage JSON for fresh installation', () => {
+ const needsInstallation = true
+
+ if (needsInstallation) {
+ mockWebContents.on('dom-ready', () => {
+ const script = `
+ (function() {
+ try {
+ const newAuthStorage = {
+ state: {
+ token: null,
+ username: null,
+ email: null,
+ user_id: null,
+ appearance: 'light',
+ language: 'system',
+ isFirstLaunch: true,
+ modelType: 'cloud',
+ cloud_model_type: 'gpt-4.1',
+ initState: 'carousel',
+ share_token: null,
+ workerListData: {}
+ },
+ version: 0
+ };
+ localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
+ console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
+ } catch (e) {
+ console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e);
+ }
+ })();
+ `
+ mockWebContents.executeJavaScript(script)
+ })
+ }
+
+ const domReadyCallback = mockWebContents.on.mock.calls[0]?.[1]
+ if (domReadyCallback) {
+ domReadyCallback()
+ }
+
+ const executedScript = mockWebContents.executeJavaScript.mock.calls[0]?.[0]
+
+ // Verify the script is wrapped in IIFE
+ expect(executedScript).toMatch(/^\s*\(\s*function\s*\(\s*\)\s*\{/)
+ expect(executedScript).toMatch(/\}\s*\)\s*\(\s*\)\s*;?\s*$/)
+
+ // Verify it has try-catch error handling
+ expect(executedScript).toContain('try {')
+ expect(executedScript).toContain('} catch (e) {')
+
+ // Verify it sets localStorage
+ expect(executedScript).toContain('localStorage.setItem(\'auth-storage\'')
+ expect(executedScript).toContain('JSON.stringify(newAuthStorage)')
+ })
+
+ it('should check localStorage properly for completed installation', () => {
+ const needsInstallation = false
+
+ if (!needsInstallation) {
+ mockWebContents.once('dom-ready', () => {
+ const script = `
+ (function() {
+ try {
+ const authStorage = localStorage.getItem('auth-storage');
+ if (authStorage) {
+ const parsed = JSON.parse(authStorage);
+ if (parsed.state && parsed.state.initState !== 'done') {
+ parsed.state.initState = 'done';
+ localStorage.setItem('auth-storage', JSON.stringify(parsed));
+ return true;
+ }
+ }
+ return false;
+ } catch (e) {
+ console.error('[ELECTRON] Failed to update initState:', e);
+ return false;
+ }
+ })();
+ `
+ mockWebContents.executeJavaScript(script)
+ })
+ }
+
+ const domReadyCallback = mockWebContents.once.mock.calls[0]?.[1]
+ if (domReadyCallback) {
+ domReadyCallback()
+ }
+
+ const executedScript = mockWebContents.executeJavaScript.mock.calls[0]?.[0]
+
+ // Verify it gets localStorage
+ expect(executedScript).toContain('localStorage.getItem(\'auth-storage\')')
+
+ // Verify it parses JSON
+ expect(executedScript).toContain('JSON.parse(authStorage)')
+
+ // Verify it checks initState
+ expect(executedScript).toContain('initState !== \'done\'')
+
+ // Verify it updates initState
+ expect(executedScript).toContain('initState = \'done\'')
+
+ // Verify it returns boolean
+ expect(executedScript).toContain('return true')
+ expect(executedScript).toContain('return false')
+ })
+ })
+
+ describe('Console Logging in Injected Scripts', () => {
+ it('should include proper console logging for fresh installation', () => {
+ const needsInstallation = true
+
+ if (needsInstallation) {
+ mockWebContents.on('dom-ready', () => {
+ const script = `
+ console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
+ console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e);
+ `
+ mockWebContents.executeJavaScript(script)
+ })
+ }
+
+ const domReadyCallback = mockWebContents.on.mock.calls[0]?.[1]
+ if (domReadyCallback) {
+ domReadyCallback()
+ }
+
+ const executedScript = mockWebContents.executeJavaScript.mock.calls[0]?.[0]
+
+ // Verify console logging is included
+ expect(executedScript).toContain('[ELECTRON PRE-INJECT]')
+ expect(executedScript).toContain('console.log')
+ expect(executedScript).toContain('console.error')
+ })
+
+ it('should include proper console logging for completed installation', () => {
+ const needsInstallation = false
+
+ if (!needsInstallation) {
+ mockWebContents.once('dom-ready', () => {
+ const script = `
+ console.log('[ELECTRON] Updating initState from', parsed.state.initState, 'to done');
+ console.log('[ELECTRON] initState updated to done, reloading page...');
+ console.error('[ELECTRON] Failed to update initState:', e);
+ `
+ mockWebContents.executeJavaScript(script)
+ })
+ }
+
+ const domReadyCallback = mockWebContents.once.mock.calls[0]?.[1]
+ if (domReadyCallback) {
+ domReadyCallback()
+ }
+
+ const executedScript = mockWebContents.executeJavaScript.mock.calls[0]?.[0]
+
+ // Verify console logging is included
+ expect(executedScript).toContain('[ELECTRON]')
+ expect(executedScript).toContain('console.log')
+ expect(executedScript).toContain('console.error')
+ })
+ })
+})
\ No newline at end of file
diff --git a/test/unit/electron/main/installationStateLogic.test.ts b/test/unit/electron/main/installationStateLogic.test.ts
new file mode 100644
index 000000000..eabf6f158
--- /dev/null
+++ b/test/unit/electron/main/installationStateLogic.test.ts
@@ -0,0 +1,357 @@
+/**
+ * Focused tests for the complex installation state detection logic in createWindow
+ * This tests the decision matrix for when installation is needed vs when it's not
+ */
+
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
+import { setupMockEnvironment } from '../../../mocks/environmentMocks'
+
+describe('createWindow - Installation State Detection Logic', () => {
+ let mockEnv: ReturnType
+ let installationStateChecker: (mockState: any) => Promise
+
+ beforeEach(() => {
+ mockEnv = setupMockEnvironment()
+
+ // Extract the installation decision logic for focused testing
+ installationStateChecker = async (mockState) => {
+ const currentVersion = mockState.app.currentVersion
+ const versionExists = mockState.filesystem.versionFileExists
+ const savedVersion = versionExists ? mockState.filesystem.versionFileContent : ''
+ const uvExists = mockState.filesystem.binariesExist['uv'] || false
+ const bunExists = mockState.filesystem.binariesExist['bun'] || false
+ const installationCompleted = mockState.filesystem.installedLockExists
+
+ return !versionExists ||
+ savedVersion !== currentVersion ||
+ !uvExists ||
+ !bunExists ||
+ !installationCompleted
+ }
+ })
+
+ afterEach(() => {
+ mockEnv.reset()
+ })
+
+ describe('Version File Scenarios', () => {
+ it('should require installation when version file does not exist', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = false
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+
+ it('should require installation when saved version differs from current version', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '0.9.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+
+ it('should not require installation when versions match', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(false)
+ })
+
+ it('should handle version file with whitespace correctly', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = ' 1.0.0 \n'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ // In the real code, version content is trimmed
+ const trimmedVersion = mockEnv.mockState.filesystem.versionFileContent.trim()
+ const needsInstallation = trimmedVersion !== mockEnv.mockState.app.currentVersion ||
+ !mockEnv.mockState.filesystem.binariesExist['uv'] ||
+ !mockEnv.mockState.filesystem.binariesExist['bun'] ||
+ !mockEnv.mockState.filesystem.installedLockExists
+
+ expect(needsInstallation).toBe(false)
+ })
+ })
+
+ describe('Binary Existence Scenarios', () => {
+ it('should require installation when uv binary is missing', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': true }
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+
+ it('should require installation when bun binary is missing', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': false }
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+
+ it('should require installation when both binaries are missing', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+ })
+
+ describe('Installation Lock File Scenarios', () => {
+ it('should require installation when lock file is missing', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
+ mockEnv.mockState.filesystem.installedLockExists = false
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+
+ it('should not require installation when lock file exists', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(false)
+ })
+ })
+
+ describe('Combined Failure Scenarios', () => {
+ it('should require installation when multiple conditions fail', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = false
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
+ mockEnv.mockState.filesystem.installedLockExists = false
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+
+ it('should require installation when version mismatch AND missing binaries', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '0.9.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': true }
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+
+ it('should require installation when only lock file is present', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = false
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+ })
+
+ describe('Edge Cases and Boundaries', () => {
+ it('should handle empty version strings', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = ''
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+
+ it('should handle version with special characters', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0-beta.1'
+ mockEnv.mockState.app.currentVersion = '1.0.0-beta.1'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(false)
+ })
+
+ it('should handle null or undefined binary states', async () => {
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = {}
+ mockEnv.mockState.filesystem.installedLockExists = true
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(true)
+ })
+ })
+
+ describe('Platform-Specific Binary Detection', () => {
+ it('should check for .exe extension on Windows', async () => {
+ mockEnv.scenarios.missingEigentDirectories()
+ mockEnv.mockState.system.platform = 'win32'
+
+ // Test that binary detection considers .exe files on Windows
+ expect(mockEnv.processUtilsMock.getBinaryName('uv')).resolves.toBe('uv.exe')
+ })
+
+ it('should not add .exe extension on macOS', async () => {
+ mockEnv.scenarios.macOSEnvironment()
+
+ expect(mockEnv.processUtilsMock.getBinaryName('uv')).resolves.toBe('uv')
+ })
+
+ it('should not add .exe extension on Linux', async () => {
+ mockEnv.scenarios.linuxEnvironment()
+
+ expect(mockEnv.processUtilsMock.getBinaryName('uv')).resolves.toBe('uv')
+ })
+ })
+
+ describe('Decision Matrix Verification', () => {
+ // This test verifies the complete decision matrix
+ const testCases = [
+ {
+ name: 'All conditions met - no installation needed',
+ setup: {
+ versionFileExists: true,
+ versionFileContent: '1.0.0',
+ currentVersion: '1.0.0',
+ uvExists: true,
+ bunExists: true,
+ installedLockExists: true
+ },
+ expectedNeedsInstallation: false
+ },
+ {
+ name: 'Missing version file',
+ setup: {
+ versionFileExists: false,
+ versionFileContent: '',
+ currentVersion: '1.0.0',
+ uvExists: true,
+ bunExists: true,
+ installedLockExists: true
+ },
+ expectedNeedsInstallation: true
+ },
+ {
+ name: 'Version mismatch',
+ setup: {
+ versionFileExists: true,
+ versionFileContent: '0.9.0',
+ currentVersion: '1.0.0',
+ uvExists: true,
+ bunExists: true,
+ installedLockExists: true
+ },
+ expectedNeedsInstallation: true
+ },
+ {
+ name: 'Missing uv binary',
+ setup: {
+ versionFileExists: true,
+ versionFileContent: '1.0.0',
+ currentVersion: '1.0.0',
+ uvExists: false,
+ bunExists: true,
+ installedLockExists: true
+ },
+ expectedNeedsInstallation: true
+ },
+ {
+ name: 'Missing bun binary',
+ setup: {
+ versionFileExists: true,
+ versionFileContent: '1.0.0',
+ currentVersion: '1.0.0',
+ uvExists: true,
+ bunExists: false,
+ installedLockExists: true
+ },
+ expectedNeedsInstallation: true
+ },
+ {
+ name: 'Missing installation lock',
+ setup: {
+ versionFileExists: true,
+ versionFileContent: '1.0.0',
+ currentVersion: '1.0.0',
+ uvExists: true,
+ bunExists: true,
+ installedLockExists: false
+ },
+ expectedNeedsInstallation: true
+ }
+ ]
+
+ testCases.forEach(({ name, setup, expectedNeedsInstallation }) => {
+ it(`should correctly handle: ${name}`, async () => {
+ // Set up the mock state according to the test case
+ mockEnv.mockState.filesystem.versionFileExists = setup.versionFileExists
+ mockEnv.mockState.filesystem.versionFileContent = setup.versionFileContent
+ mockEnv.mockState.app.currentVersion = setup.currentVersion
+ mockEnv.mockState.filesystem.binariesExist = {
+ 'uv': setup.uvExists,
+ 'bun': setup.bunExists
+ }
+ mockEnv.mockState.filesystem.installedLockExists = setup.installedLockExists
+
+ const needsInstallation = await installationStateChecker(mockEnv.mockState)
+ expect(needsInstallation).toBe(expectedNeedsInstallation)
+ })
+ })
+ })
+
+ describe('Logging Verification', () => {
+ it('should log installation check results', async () => {
+ // This test ensures that the installation decision logic provides proper logging
+ const mockState = mockEnv.mockState
+
+ const logData = {
+ needsInstallation: await installationStateChecker(mockState),
+ versionExists: mockState.filesystem.versionFileExists,
+ versionMatch: mockState.filesystem.versionFileContent === mockState.app.currentVersion,
+ uvExists: mockState.filesystem.binariesExist['uv'] || false,
+ bunExists: mockState.filesystem.binariesExist['bun'] || false,
+ installationCompleted: mockState.filesystem.installedLockExists
+ }
+
+ // Verify that all required data for logging is available
+ expect(logData).toHaveProperty('needsInstallation')
+ expect(logData).toHaveProperty('versionExists')
+ expect(logData).toHaveProperty('versionMatch')
+ expect(logData).toHaveProperty('uvExists')
+ expect(logData).toHaveProperty('bunExists')
+ expect(logData).toHaveProperty('installationCompleted')
+
+ // Verify the logic is consistent
+ const expectedNeedsInstallation = !logData.versionExists ||
+ !logData.versionMatch ||
+ !logData.uvExists ||
+ !logData.bunExists ||
+ !logData.installationCompleted
+
+ expect(logData.needsInstallation).toBe(expectedNeedsInstallation)
+ })
+ })
+})
\ No newline at end of file
diff --git a/test/unit/electron/main/processUtilsDemo.test.ts b/test/unit/electron/main/processUtilsDemo.test.ts
new file mode 100644
index 000000000..f560d86f7
--- /dev/null
+++ b/test/unit/electron/main/processUtilsDemo.test.ts
@@ -0,0 +1,199 @@
+/**
+ * Simple demonstration test for the new process utilities mocking
+ * This shows how to test the functions from process.ts with our enhanced mocks
+ */
+
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
+import {
+ setupMockEnvironment,
+ createFileSystemMock,
+ createProcessMock,
+ createElectronAppMock,
+ createOsMock,
+ createPathMock,
+ createLogMock,
+ createProcessUtilsMock
+} from '../../../mocks/environmentMocks'
+
+// Set up vi.mock calls at the top level to avoid hoisting issues
+vi.mock('fs', () => createFileSystemMock())
+vi.mock('child_process', () => createProcessMock())
+vi.mock('os', () => ({ default: createOsMock() }))
+vi.mock('path', () => ({ default: createPathMock() }))
+vi.mock('electron', () => ({
+ app: createElectronAppMock(),
+ BrowserWindow: vi.fn()
+}))
+vi.mock('electron-log', () => ({ default: createLogMock() }))
+vi.mock('../../../../electron/main/utils/process', () => createProcessUtilsMock())
+
+describe('Process Utils Mocking Demo', () => {
+ let mockEnv: ReturnType
+
+ beforeEach(() => {
+ mockEnv = setupMockEnvironment()
+ })
+
+ afterEach(() => {
+ mockEnv.reset()
+ })
+
+ describe('Binary Path Functions', () => {
+ it('should return correct binary paths based on platform', async () => {
+ // Test Windows binary naming
+ mockEnv.mockState.system.platform = 'win32'
+
+ const uvBinaryName = await mockEnv.processUtilsMock.getBinaryName('uv')
+ expect(uvBinaryName).toBe('uv.exe')
+
+ const uvBinaryPath = await mockEnv.processUtilsMock.getBinaryPath('uv')
+ expect(uvBinaryPath).toContain('.eigent/bin')
+ expect(uvBinaryPath).toContain('uv.exe')
+ })
+
+ it('should return correct binary paths for macOS', async () => {
+ mockEnv.scenarios.macOSEnvironment()
+
+ const uvBinaryName = await mockEnv.processUtilsMock.getBinaryName('uv')
+ expect(uvBinaryName).toBe('uv')
+
+ const uvBinaryPath = await mockEnv.processUtilsMock.getBinaryPath('uv')
+ expect(uvBinaryPath).toContain('.eigent/bin')
+ expect(uvBinaryPath).toContain('/uv')
+ expect(uvBinaryPath).not.toContain('.exe')
+ })
+ })
+
+ describe('Directory Path Functions', () => {
+ it('should return correct backend path for development mode', () => {
+ mockEnv.mockState.app.isPackaged = false
+
+ const backendPath = mockEnv.processUtilsMock.getBackendPath()
+ expect(backendPath).toContain('/backend')
+ expect(backendPath).not.toContain('resources')
+ })
+
+ it('should return correct backend path for packaged app', () => {
+ mockEnv.scenarios.packagedApp()
+
+ const backendPath = mockEnv.processUtilsMock.getBackendPath()
+ expect(backendPath).toContain('backend')
+ // In packaged mode, it should use resources path
+ expect(mockEnv.mockState.app.isPackaged).toBe(true)
+ })
+
+ it('should return correct cache paths', () => {
+ const cachePath = mockEnv.processUtilsMock.getCachePath('models')
+ expect(cachePath).toContain('.eigent/cache/models')
+ })
+
+ it('should return correct venv paths', () => {
+ const venvPath = mockEnv.processUtilsMock.getVenvPath('1.0.0')
+ expect(venvPath).toContain('.eigent/venvs/backend-1.0.0')
+ })
+ })
+
+ describe('Binary Existence Checking', () => {
+ it('should correctly check binary existence', async () => {
+ // Set up scenario where uv exists but bun doesn't
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': false }
+
+ const uvExists = await mockEnv.processUtilsMock.isBinaryExists('uv')
+ const bunExists = await mockEnv.processUtilsMock.isBinaryExists('bun')
+
+ expect(uvExists).toBe(true)
+ expect(bunExists).toBe(false)
+ })
+ })
+
+ describe('Old Venv Cleanup', () => {
+ it('should cleanup old venvs correctly', async () => {
+ // Set up scenario with multiple old venvs
+ mockEnv.scenarios.multipleOldVenvs('1.0.0')
+
+ const initialOldVenvs = [...mockEnv.mockState.filesystem.oldVenvsExist]
+ expect(initialOldVenvs).toContain('backend-0.9.0')
+ expect(initialOldVenvs).toContain('backend-0.9.5')
+ expect(initialOldVenvs).toContain('backend-1.0.1-beta')
+
+ // Run cleanup
+ await mockEnv.processUtilsMock.cleanupOldVenvs('1.0.0')
+
+ // Should keep current version but remove others
+ const remainingVenvs = mockEnv.mockState.filesystem.oldVenvsExist
+ console.log(remainingVenvs);
+
+
+ expect(remainingVenvs).toContain('backend-1.0.0')
+ expect(remainingVenvs).not.toContain('backend-0.9.0')
+ expect(remainingVenvs).not.toContain('backend-0.9.5')
+ expect(remainingVenvs).not.toContain('backend-1.0.1-beta')
+ })
+ })
+
+ describe('Installation Decision Matrix', () => {
+ it('should correctly determine when installation is needed', () => {
+ // Test the decision logic that createWindow uses
+ const checkInstallationNeeded = (mockState: any) => {
+ const currentVersion = mockState.app.currentVersion
+ const versionExists = mockState.filesystem.versionFileExists
+ const savedVersion = versionExists ? mockState.filesystem.versionFileContent : ''
+ const uvExists = mockState.filesystem.binariesExist['uv'] || false
+ const bunExists = mockState.filesystem.binariesExist['bun'] || false
+ const installationCompleted = mockState.filesystem.installedLockExists
+
+ return !versionExists ||
+ savedVersion !== currentVersion ||
+ !uvExists ||
+ !bunExists ||
+ !installationCompleted
+ }
+
+ // Test fresh install scenario
+ mockEnv.scenarios.freshInstall()
+ expect(checkInstallationNeeded(mockEnv.mockState)).toBe(true)
+
+ // Test version update scenario
+ mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
+ expect(checkInstallationNeeded(mockEnv.mockState)).toBe(true)
+
+ // Test all good scenario
+ mockEnv.mockState.filesystem.versionFileExists = true
+ mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+ mockEnv.mockState.app.currentVersion = '1.0.0'
+ mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
+ mockEnv.mockState.filesystem.installedLockExists = true
+ expect(checkInstallationNeeded(mockEnv.mockState)).toBe(false)
+ })
+ })
+
+ describe('File System Operations', () => {
+ it('should handle directory creation correctly', () => {
+ // Test .eigent directory creation
+ mockEnv.scenarios.missingEigentDirectories()
+
+ expect(mockEnv.mockState.filesystem.eigentDirExists).toBe(false)
+ expect(mockEnv.mockState.filesystem.eigentBinDirExists).toBe(false)
+
+ // Simulate directory creation
+ mockEnv.fsMock.mkdirSync('/mock/home/.eigent', { recursive: true })
+ mockEnv.fsMock.mkdirSync('/mock/home/.eigent/bin', { recursive: true })
+
+ expect(mockEnv.mockState.filesystem.eigentDirExists).toBe(true)
+ expect(mockEnv.mockState.filesystem.eigentBinDirExists).toBe(true)
+ })
+
+ it('should handle file operations correctly', () => {
+ // Test version file operations
+ expect(mockEnv.mockState.filesystem.versionFileExists).toBe(true)
+
+ // Write new version
+ mockEnv.fsMock.writeFileSync('/path/to/version.txt', '2.0.0')
+ expect(mockEnv.mockState.filesystem.versionFileContent).toBe('2.0.0')
+
+ // Delete version file
+ mockEnv.fsMock.unlinkSync('/path/to/version.txt')
+ expect(mockEnv.mockState.filesystem.versionFileExists).toBe(false)
+ })
+ })
+})
\ No newline at end of file
diff --git a/test/unit/electron/main/windowLifecycle.test.ts b/test/unit/electron/main/windowLifecycle.test.ts
new file mode 100644
index 000000000..8f19f2b83
--- /dev/null
+++ b/test/unit/electron/main/windowLifecycle.test.ts
@@ -0,0 +1,407 @@
+/**
+ * Tests for window event setup and lifecycle management in createWindow function
+ * Covers dev tools shortcuts, external link handling, before close handling,
+ * auto-update integration, webview manager, and file reader initialization
+ */
+
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
+import { setupMockEnvironment } from '../../../mocks/environmentMocks'
+
+describe('createWindow - Window Event Setup and Lifecycle', () => {
+ let mockEnv: ReturnType
+ let mockWebContents: any
+ let mockWindow: any
+ let mockFileReader: any
+ let mockWebViewManager: any
+ let mockUpdate: any
+ let mockMenu: any
+
+ beforeEach(() => {
+ mockEnv = setupMockEnvironment()
+
+ // Mock webContents
+ mockWebContents = {
+ on: vi.fn(),
+ once: vi.fn(),
+ executeJavaScript: vi.fn(),
+ send: vi.fn(),
+ loadURL: vi.fn(),
+ loadFile: vi.fn(),
+ openDevTools: vi.fn(),
+ toggleDevTools: vi.fn()
+ }
+
+ // Mock window
+ mockWindow = {
+ webContents: mockWebContents,
+ reload: vi.fn()
+ }
+
+ // Mock FileReader class
+ mockFileReader = vi.fn()
+
+ // Mock WebViewManager class
+ mockWebViewManager = vi.fn().mockImplementation(() => ({
+ createWebview: vi.fn()
+ }))
+
+ // Mock update function
+ mockUpdate = vi.fn()
+
+ // Mock Menu
+ mockMenu = {
+ setApplicationMenu: vi.fn()
+ }
+
+ // Reset all mocks
+ vi.clearAllMocks()
+ })
+
+ afterEach(() => {
+ mockEnv.reset()
+ })
+
+ describe('FileReader and WebViewManager Initialization', () => {
+ it.skip('should create 8 webviews with correct IDs', () => {
+ const webViewManager = new mockWebViewManager(mockWindow)
+ const instance = mockWebViewManager.mock.instances[0]
+
+ // Simulate the loop that creates webviews
+ for (let i = 1; i <= 8; i++) {
+ instance.createWebview(i === 1 ? undefined : i.toString())
+ }
+
+ expect(instance.createWebview).toHaveBeenCalledTimes(8)
+ expect(instance.createWebview).toHaveBeenNthCalledWith(1, undefined)
+ expect(instance.createWebview).toHaveBeenNthCalledWith(2, '2')
+ expect(instance.createWebview).toHaveBeenNthCalledWith(3, '3')
+ expect(instance.createWebview).toHaveBeenNthCalledWith(4, '4')
+ expect(instance.createWebview).toHaveBeenNthCalledWith(5, '5')
+ expect(instance.createWebview).toHaveBeenNthCalledWith(6, '6')
+ expect(instance.createWebview).toHaveBeenNthCalledWith(7, '7')
+ expect(instance.createWebview).toHaveBeenNthCalledWith(8, '8')
+ })
+ })
+
+ describe('Window Event Listeners Setup', () => {
+ it('should disable application menu', () => {
+ // Simulate setupWindowEventListeners
+ mockMenu.setApplicationMenu(null)
+
+ expect(mockMenu.setApplicationMenu).toHaveBeenCalledWith(null)
+ })
+
+ it('should set up application menu only once', () => {
+ // Simulate multiple calls to setupWindowEventListeners
+ mockMenu.setApplicationMenu(null)
+ mockMenu.setApplicationMenu(null)
+
+ expect(mockMenu.setApplicationMenu).toHaveBeenCalledTimes(2)
+ expect(mockMenu.setApplicationMenu).toHaveBeenCalledWith(null)
+ })
+ })
+
+ describe('DevTools Shortcuts Setup', () => {
+ it('should set up before-input-event listener for dev tools shortcuts', () => {
+ // Simulate setupDevToolsShortcuts
+ mockWebContents.on('before-input-event', expect.any(Function))
+
+ expect(mockWebContents.on).toHaveBeenCalledWith('before-input-event', expect.any(Function))
+ })
+
+ it('should handle F12 key to toggle dev tools', () => {
+ let beforeInputCallback: any
+
+ mockWebContents.on.mockImplementation((event: string, callback: any) => {
+ if (event === 'before-input-event') {
+ beforeInputCallback = callback
+ }
+ })
+
+ // Simulate setupDevToolsShortcuts
+ mockWebContents.on('before-input-event', (event: any, input: any) => {
+ if (input.key === 'F12' && input.type === 'keyDown') {
+ mockWebContents.toggleDevTools()
+ }
+ })
+
+ // Trigger F12 key
+ if (beforeInputCallback) {
+ const mockEvent = { preventDefault: vi.fn() }
+ const mockInput = { key: 'F12', type: 'keyDown' }
+ beforeInputCallback(mockEvent, mockInput)
+ }
+
+ expect(mockWebContents.toggleDevTools).toHaveBeenCalled()
+ })
+
+ it('should handle Ctrl+Shift+I to toggle dev tools on Windows/Linux', () => {
+ let beforeInputCallback: any
+
+ mockWebContents.on.mockImplementation((event: string, callback: any) => {
+ if (event === 'before-input-event') {
+ beforeInputCallback = callback
+ }
+ })
+
+ // Simulate setupDevToolsShortcuts
+ mockWebContents.on('before-input-event', (event: any, input: any) => {
+ if (input.control && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') {
+ mockWebContents.toggleDevTools()
+ }
+ })
+
+ // Trigger Ctrl+Shift+I
+ if (beforeInputCallback) {
+ const mockEvent = { preventDefault: vi.fn() }
+ const mockInput = {
+ control: true,
+ shift: true,
+ key: 'I',
+ type: 'keyDown'
+ }
+ beforeInputCallback(mockEvent, mockInput)
+ }
+
+ expect(mockWebContents.toggleDevTools).toHaveBeenCalled()
+ })
+
+ it('should handle Cmd+Shift+I to toggle dev tools on Mac', () => {
+ let beforeInputCallback: any
+
+ mockWebContents.on.mockImplementation((event: string, callback: any) => {
+ if (event === 'before-input-event') {
+ beforeInputCallback = callback
+ }
+ })
+
+ // Simulate setupDevToolsShortcuts
+ mockWebContents.on('before-input-event', (event: any, input: any) => {
+ if (input.meta && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') {
+ mockWebContents.toggleDevTools()
+ }
+ })
+
+ // Trigger Cmd+Shift+I
+ if (beforeInputCallback) {
+ const mockEvent = { preventDefault: vi.fn() }
+ const mockInput = {
+ meta: true,
+ shift: true,
+ key: 'I',
+ type: 'keyDown'
+ }
+ beforeInputCallback(mockEvent, mockInput)
+ }
+
+ expect(mockWebContents.toggleDevTools).toHaveBeenCalled()
+ })
+
+ it('should not trigger dev tools on key up events', () => {
+ let beforeInputCallback: any
+
+ mockWebContents.on.mockImplementation((event: string, callback: any) => {
+ if (event === 'before-input-event') {
+ beforeInputCallback = callback
+ }
+ })
+
+ // Simulate setupDevToolsShortcuts
+ mockWebContents.on('before-input-event', (event: any, input: any) => {
+ if (input.key === 'F12' && input.type === 'keyDown') {
+ mockWebContents.toggleDevTools()
+ }
+ })
+
+ // Trigger F12 key up (should not toggle)
+ if (beforeInputCallback) {
+ const mockEvent = { preventDefault: vi.fn() }
+ const mockInput = { key: 'F12', type: 'keyUp' }
+ beforeInputCallback(mockEvent, mockInput)
+ }
+
+ expect(mockWebContents.toggleDevTools).not.toHaveBeenCalled()
+ })
+
+ it('should not trigger dev tools on wrong key combinations', () => {
+ let beforeInputCallback: any
+
+ mockWebContents.on.mockImplementation((event: string, callback: any) => {
+ if (event === 'before-input-event') {
+ beforeInputCallback = callback
+ }
+ })
+
+ // Simulate setupDevToolsShortcuts
+ mockWebContents.on('before-input-event', (event: any, input: any) => {
+ if (input.control && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') {
+ mockWebContents.toggleDevTools()
+ }
+ })
+
+ // Trigger wrong combination (Ctrl+I without Shift)
+ if (beforeInputCallback) {
+ const mockEvent = { preventDefault: vi.fn() }
+ const mockInput = {
+ control: true,
+ shift: false,
+ key: 'I',
+ type: 'keyDown'
+ }
+ beforeInputCallback(mockEvent, mockInput)
+ }
+
+ expect(mockWebContents.toggleDevTools).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('Auto-Update Integration', () => {
+ it('should call update function with window reference', () => {
+ // Simulate auto-update setup
+ mockUpdate(mockWindow)
+
+ expect(mockUpdate).toHaveBeenCalledWith(mockWindow)
+ })
+
+ it('should call update function only once', () => {
+ // Simulate auto-update setup
+ mockUpdate(mockWindow)
+
+ expect(mockUpdate).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ describe('Event Handler Organization', () => {
+ it('should set up event handlers in correct order', () => {
+ const eventSetupOrder: string[] = []
+
+ // Mock all the setup functions to track order
+ const setupWindowEventListeners = () => {
+ eventSetupOrder.push('windowEventListeners')
+ mockMenu.setApplicationMenu(null)
+ }
+
+ const setupDevToolsShortcuts = () => {
+ eventSetupOrder.push('devToolsShortcuts')
+ mockWebContents.on('before-input-event', vi.fn())
+ }
+
+ const setupExternalLinkHandling = () => {
+ eventSetupOrder.push('externalLinkHandling')
+ }
+
+ const handleBeforeClose = () => {
+ eventSetupOrder.push('beforeClose')
+ }
+
+ // Simulate the order in createWindow
+ setupWindowEventListeners()
+ setupDevToolsShortcuts()
+ setupExternalLinkHandling()
+ handleBeforeClose()
+
+ expect(eventSetupOrder).toEqual([
+ 'windowEventListeners',
+ 'devToolsShortcuts',
+ 'externalLinkHandling',
+ 'beforeClose'
+ ])
+ })
+ })
+
+ describe('Window State Management', () => {
+ it('should handle window ready state correctly', async () => {
+ let didFinishLoadCallback: (() => void) | undefined
+
+ // Mock the did-finish-load event listener
+ mockWebContents.once.mockImplementation((event: string, callback: () => void) => {
+ if (event === 'did-finish-load') {
+ didFinishLoadCallback = callback
+ }
+ })
+
+ // Simulate waiting for window ready
+ const windowReadyPromise = new Promise(resolve => {
+ mockWebContents.once('did-finish-load', () => {
+ resolve()
+ })
+ })
+
+ // Trigger the event
+ if (didFinishLoadCallback) {
+ didFinishLoadCallback()
+ }
+
+ // Should resolve without throwing
+ await expect(windowReadyPromise).resolves.toBeUndefined()
+ })
+
+ it('should log appropriate messages during window setup', () => {
+ // In a real test, you would verify that appropriate log messages are called
+ // This ensures the window setup process is properly logged
+ const mockLog = {
+ info: vi.fn(),
+ error: vi.fn()
+ }
+
+ // Simulate logging calls that would happen during window setup
+ mockLog.info('Window content loaded, starting dependency check immediately...')
+ mockLog.info('.eigent directory structure ensured')
+
+ expect(mockLog.info).toHaveBeenCalledWith(
+ 'Window content loaded, starting dependency check immediately...'
+ )
+ expect(mockLog.info).toHaveBeenCalledWith(
+ '.eigent directory structure ensured'
+ )
+ })
+ })
+
+ describe('Integration Points', () => {
+ it('should properly coordinate between file reader and webview manager', () => {
+ const fileReader = new mockFileReader(mockWindow)
+ const webViewManager = new mockWebViewManager(mockWindow)
+
+ // Both should be initialized with the same window
+ expect(mockFileReader).toHaveBeenCalledWith(mockWindow)
+ expect(mockWebViewManager).toHaveBeenCalledWith(mockWindow)
+ })
+
+ it('should handle window initialization errors gracefully', () => {
+ // Mock FileReader to throw during initialization
+ mockFileReader.mockImplementation(() => {
+ throw new Error('FileReader initialization failed')
+ })
+
+ // Should handle gracefully in real implementation
+ expect(() => {
+ try {
+ new mockFileReader(mockWindow)
+ } catch (error) {
+ // Log error but don't stop execution
+ console.error('FileReader initialization error:', error)
+ }
+ }).not.toThrow()
+ })
+ })
+
+ describe('Memory Management', () => {
+ it('should properly clean up event listeners when window is destroyed', () => {
+ // In a real scenario, you would test that event listeners are removed
+ // when the window is closed to prevent memory leaks
+
+ const mockRemoveListener = vi.fn()
+ mockWebContents.removeListener = mockRemoveListener
+
+ // Simulate cleanup
+ const cleanup = () => {
+ mockWebContents.removeListener('before-input-event', vi.fn())
+ mockWebContents.removeListener('dom-ready', vi.fn())
+ }
+
+ cleanup()
+
+ expect(mockRemoveListener).toHaveBeenCalledTimes(2)
+ })
+ })
+})
\ No newline at end of file
diff --git a/test/unit/examples/installationFlow.test.ts b/test/unit/examples/installationFlow.test.ts
new file mode 100644
index 000000000..de3f9717f
--- /dev/null
+++ b/test/unit/examples/installationFlow.test.ts
@@ -0,0 +1,382 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
+import { renderHook, act } from '@testing-library/react'
+import React from 'react'
+import { createTestEnvironment, waitForInstallationState } from '../../mocks/testUtils'
+import { useInstallationStore } from '../../../src/store/installationStore'
+
+/**
+ * Example test file demonstrating how to use the test environment
+ * This shows all the main scenarios you wanted to test
+ */
+describe('Installation Flow Examples', () => {
+ let testEnv: ReturnType
+
+ beforeEach(() => {
+ testEnv = createTestEnvironment()
+ useInstallationStore.getState().reset()
+ })
+
+ afterEach(() => {
+ testEnv.reset()
+ })
+
+ describe('Main Test Scenarios', () => {
+ it('should handle when .venv is removed', async () => {
+ // Set up scenario
+ testEnv.scenarios.venvRemoved()
+
+ // Verify scenario is set up correctly
+ expect(testEnv.inspect.verifyScenario('venvRemoved')).toBe(true)
+ expect(testEnv.inspect.getInstallationState().venvExists).toBe(false)
+
+ // Trigger installation
+ const result = await testEnv.electronAPI.checkAndInstallDepsOnUpdate()
+
+ // Should trigger installation since .venv is missing
+ expect(result.success).toBe(true)
+ console.log(result);
+
+ expect(result.message).toContain('Dependencies installed successfully')
+ })
+
+ it('should handle when version file is different', async () => {
+ // Set up version update scenario
+ testEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
+
+ // Verify scenario
+ expect(testEnv.inspect.verifyScenario('versionUpdate')).toBe(true)
+
+ // Trigger installation
+ const result = await testEnv.electronAPI.checkAndInstallDepsOnUpdate()
+
+ // Should install due to version mismatch
+ expect(result.success).toBe(true)
+ expect(result.message).toContain('Dependencies installed successfully after update')
+ })
+
+ it('should handle when uvicorn starts installing deps after page is loaded', async () => {
+ // Set up uvicorn dependency installation scenario
+ testEnv.scenarios.uvicornDepsInstall()
+
+ const { result } = renderHook(() => useInstallationStore())
+
+ // Set up event listeners manually for this test
+ const startInstallation = result.current.startInstallation
+ const addLog = result.current.addLog
+ const setSuccess = result.current.setSuccess
+ const setError = result.current.setError
+
+ // Set up the electron API event handlers to connect to the store
+ testEnv.electronAPI.onInstallDependenciesStart(() => {
+ act(() => {
+ startInstallation()
+ })
+ })
+
+ testEnv.electronAPI.onInstallDependenciesLog((data: { type: string; data: string }) => {
+ act(() => {
+ addLog({
+ type: data.type as 'stdout' | 'stderr',
+ data: data.data,
+ timestamp: new Date(),
+ })
+ })
+ })
+
+ testEnv.electronAPI.onInstallDependenciesComplete((data: { success: boolean; error?: string }) => {
+ act(() => {
+ if (data.success) {
+ setSuccess()
+ } else {
+ setError(data.error || 'Installation failed')
+ }
+ })
+ })
+
+ // Simulate uvicorn startup that triggers dependency detection
+ await act(async () => {
+ // Use the electron mock's simulation methods instead of calling detectInstallationLogs directly
+ testEnv.electronAPI.simulateInstallationStart()
+
+ // Wait a bit for state to update
+ await new Promise(resolve => setTimeout(resolve, 50))
+ })
+
+ // Should start installation
+ expect(result.current.state).toBe('installing')
+ console.log("State after startInstalling() ", result.current);
+
+
+ // Simulate UV sync/run command being executed
+ await act(async () => {
+ testEnv.electronAPI.simulateInstallationLog('stdout', 'Resolved 45 packages in 2.1s')
+ testEnv.electronAPI.simulateInstallationLog('stdout', 'Downloaded 12 packages in 1.3s')
+ testEnv.electronAPI.simulateInstallationLog('stdout', 'Installing packages...')
+
+ // Wait a bit for state to update
+ await new Promise(resolve => setTimeout(resolve, 50))
+ })
+
+ // Should still be installing with logs
+ expect(result.current.state).toBe('installing')
+ expect(result.current.logs.length).toBeGreaterThan(0)
+
+ // Simulate uvicorn completing successfully
+ await act(async () => {
+ testEnv.electronAPI.simulateInstallationLog('stdout', 'Uvicorn running on http://127.0.0.1:8000')
+ testEnv.electronAPI.simulateInstallationComplete(true)
+ await new Promise(resolve => setTimeout(resolve, 50))
+ })
+
+ // Should complete successfully
+ expect(result.current.state).toBe('completed')
+ })
+ })
+
+ describe('All Installation UI States', () => {
+ it('should test idle state', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ expect(result.current.state).toBe('idle')
+ expect(result.current.progress).toBe(20)
+ expect(result.current.logs).toEqual([])
+ expect(result.current.error).toBeUndefined()
+ expect(result.current.isVisible).toBe(false)
+ })
+
+ it('should test installing state', async () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ expect(result.current.state).toBe('installing')
+ expect(result.current.isVisible).toBe(true)
+ expect(result.current.progress).toBe(20)
+ })
+
+ it('should test error state', async () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ act(() => {
+ result.current.setError('Installation failed')
+ })
+
+ expect(result.current.state).toBe('error')
+ expect(result.current.error).toBe('Installation failed')
+ expect(result.current.logs).toHaveLength(1)
+ expect(result.current.logs[0].type).toBe('stderr')
+ })
+
+ it('should test completed state', async () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ act(() => {
+ result.current.setSuccess()
+ })
+
+ expect(result.current.state).toBe('completed')
+ expect(result.current.progress).toBe(100)
+ })
+
+ it('should test retry after error', async () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ // Start and fail installation
+ act(() => {
+ result.current.startInstallation()
+ result.current.setError('Installation failed')
+ })
+
+ expect(result.current.state).toBe('error')
+
+ // Retry installation
+ act(() => {
+ result.current.retryInstallation()
+ })
+
+ expect(result.current.state).toBe('installing')
+ expect(result.current.error).toBeUndefined()
+ expect(result.current.logs).toEqual([])
+ })
+ })
+
+ describe('Complete Installation Flows', () => {
+ it('should handle fresh installation flow', async () => {
+ testEnv.scenarios.freshInstall()
+
+ const { result } = renderHook(() => useInstallationStore())
+
+ // Start installation
+ await act(async () => {
+ await result.current.performInstallation()
+ })
+
+ // Should complete successfully
+ await waitForInstallationState(() => result.current, 'completed', 1000)
+ expect(result.current.state).toBe('completed')
+ })
+
+ it('should handle installation with simulation', async () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ // Start installation manually
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ testEnv.electronAPI.onInstallDependenciesStart(() => {
+ act(() => {
+ result.current.startInstallation()
+ })
+ })
+
+ testEnv.electronAPI.onInstallDependenciesLog((data: { type: string; data: string }) => {
+ act(() => {
+ result.current.addLog({
+ type: data.type as 'stdout' | 'stderr',
+ data: data.data,
+ timestamp: new Date(),
+ })
+ })
+ })
+
+ testEnv.electronAPI.onInstallDependenciesComplete((data: { success: boolean; error?: string }) => {
+ act(() => {
+ if (data.success) {
+ result.current.setSuccess()
+ } else {
+ result.current.setError(data.error || 'Installation failed')
+ }
+ })
+ })
+
+ console.log("State before success installation, ", result.current)
+ // Simulate successful installation flow
+ await testEnv.simulate.successfulInstallation(50)
+
+ // Wait for completion
+ await waitForInstallationState(() => result.current, 'completed', 1000)
+ console.log("State after success installation, ", result.current)
+
+ expect(result.current.state).toBe('completed')
+ expect(result.current.logs.length).toBeGreaterThan(0)
+ })
+
+ it('should handle installation failure with retry', async () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ // Start installation
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ testEnv.electronAPI.onInstallDependenciesStart(() => {
+ act(() => {
+ result.current.startInstallation()
+ })
+ })
+
+ testEnv.electronAPI.onInstallDependenciesLog((data: { type: string; data: string }) => {
+ act(() => {
+ result.current.addLog({
+ type: data.type as 'stdout' | 'stderr',
+ data: data.data,
+ timestamp: new Date(),
+ })
+ })
+ })
+
+ testEnv.electronAPI.onInstallDependenciesComplete((data: { success: boolean; error?: string }) => {
+ act(() => {
+ if (data.success) {
+ result.current.setSuccess()
+ } else {
+ result.current.setError(data.error || 'Installation failed')
+ }
+ })
+ })
+
+ // Simulate failed installation
+ await testEnv.simulate.failedInstallation(50, 'Network error')
+
+ // Wait for error state
+ await waitForInstallationState(() => result.current, 'error', 1000)
+ console.log("State after event listened", result.current);
+
+ expect(result.current.state).toBe('error')
+ expect(result.current.error).toBe('Network error')
+
+ // Fix the environment and retry
+ testEnv.scenarios.allGood()
+
+ act(() => {
+ result.current.retryInstallation()
+ })
+
+ // Simulate successful retry
+ await testEnv.simulate.successfulInstallation(50)
+
+ // Should complete successfully
+ await waitForInstallationState(() => result.current, 'completed', 1000)
+ expect(result.current.state).toBe('completed')
+ })
+ })
+
+ describe('State Inspection', () => {
+ it('should provide useful state inspection', () => {
+ testEnv.scenarios.freshInstall()
+
+ const state = testEnv.inspect.getInstallationState()
+
+ expect(state.venvExists).toBe(false)
+ expect(state.toolsAvailable).toBe(false)
+ expect(state.isInstalling).toBe(false)
+ expect(state.hasLockFiles).toBe(false)
+ })
+
+ it('should verify scenario setup', () => {
+ testEnv.scenarios.versionUpdate()
+ expect(testEnv.inspect.verifyScenario('versionUpdate')).toBe(true)
+
+ testEnv.scenarios.freshInstall()
+ expect(testEnv.inspect.verifyScenario('freshInstall')).toBe(true)
+ expect(testEnv.inspect.verifyScenario('versionUpdate')).toBe(false)
+ })
+ })
+
+ describe('Environment Changes During Tests', () => {
+ it('should allow changing environment state during test', async () => {
+ // Start with fresh install
+ testEnv.scenarios.freshInstall()
+ expect(testEnv.inspect.getInstallationState().venvExists).toBe(false)
+
+ // Simulate .venv being created
+ testEnv.electronAPI.mockState.venvExists = true
+ testEnv.mockEnv.mockState.filesystem.venvExists = true
+
+ expect(testEnv.inspect.getInstallationState().venvExists).toBe(true)
+
+ // Simulate version file being created
+ testEnv.electronAPI.simulateVersionChange('1.0.0')
+ testEnv.mockEnv.mockState.filesystem.versionFileExists = true
+ testEnv.mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
+
+ // Now environment should be in 'all good' state
+ const state = testEnv.inspect.getInstallationState()
+ expect(state.venvExists).toBe(true)
+ })
+ })
+})
+
+// You can run this test file with:
+// npm test test/unit/examples/installationFlow.test.ts
\ No newline at end of file
diff --git a/test/unit/hooks/useInstallationSetup.test.ts b/test/unit/hooks/useInstallationSetup.test.ts
new file mode 100644
index 000000000..afc03e044
--- /dev/null
+++ b/test/unit/hooks/useInstallationSetup.test.ts
@@ -0,0 +1,404 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
+import { renderHook, act } from '@testing-library/react'
+import { useInstallationSetup } from '../../../src/hooks/useInstallationSetup'
+import { useInstallationStore } from '../../../src/store/installationStore'
+import { useAuthStore } from '../../../src/store/authStore'
+import { setupElectronMocks, TestScenarios, type MockedElectronAPI } from '../../mocks/electronMocks'
+
+// Mock the stores
+vi.mock('../../../src/store/installationStore')
+vi.mock('../../../src/store/authStore')
+
+describe('useInstallationSetup Hook', () => {
+ let electronAPI: MockedElectronAPI
+ let mockInstallationStore: any
+ let mockAuthStore: any
+
+ beforeEach(() => {
+ // Set up electron mocks
+ const mocks = setupElectronMocks()
+ electronAPI = mocks.electronAPI
+
+ // Mock installation store
+ mockInstallationStore = {
+ startInstallation: vi.fn(),
+ addLog: vi.fn(),
+ setSuccess: vi.fn(),
+ setError: vi.fn(),
+ }
+
+ // Mock auth store
+ mockAuthStore = {
+ initState: 'done',
+ setInitState: vi.fn(),
+ }
+
+ // Set up mock implementations
+ vi.mocked(useInstallationStore).mockImplementation((selector: any) => {
+ if (typeof selector === 'function') {
+ return selector(mockInstallationStore)
+ }
+ return mockInstallationStore
+ })
+
+ vi.mocked(useAuthStore).mockReturnValue(mockAuthStore)
+
+ // Mock console.log to avoid noise in tests
+ vi.spyOn(console, 'log').mockImplementation(() => {})
+ vi.spyOn(console, 'error').mockImplementation(() => {})
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ electronAPI.reset()
+ })
+
+ describe('Initial Setup', () => {
+ it('should check tool installation status on mount', async () => {
+ // Mock IPC response for tool check
+ electronAPI.mockState.toolInstalled = true
+
+ renderHook(() => useInstallationSetup())
+
+ await vi.waitFor(() => {
+ expect(window.ipcRenderer.invoke).toHaveBeenCalledWith('check-tool-installed')
+ })
+ })
+
+ it('should check backend installation status on mount', async () => {
+ renderHook(() => useInstallationSetup())
+
+ await vi.waitFor(() => {
+ expect(electronAPI.getInstallationStatus).toHaveBeenCalled()
+ })
+ })
+
+ it('should start installation if backend installation is in progress', async () => {
+ electronAPI.mockState.isInstalling = true
+
+ renderHook(() => useInstallationSetup())
+
+ await vi.waitFor(() => {
+ expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
+ })
+ })
+
+ it('should set initState to carousel if tool is not installed', async () => {
+ // Mock tool not installed
+ window.ipcRenderer.invoke = vi.fn().mockResolvedValue({
+ success: true,
+ isInstalled: false
+ })
+
+ renderHook(() => useInstallationSetup())
+
+ await vi.waitFor(() => {
+ expect(mockAuthStore.setInitState).toHaveBeenCalledWith('carousel')
+ })
+ })
+ })
+
+ describe('Electron IPC Event Handling', () => {
+ it('should register all required event listeners', () => {
+ renderHook(() => useInstallationSetup())
+
+ expect(electronAPI.onInstallDependenciesStart).toHaveBeenCalled()
+ expect(electronAPI.onInstallDependenciesLog).toHaveBeenCalled()
+ expect(electronAPI.onInstallDependenciesComplete).toHaveBeenCalled()
+ })
+
+ it('should handle install-dependencies-start event', () => {
+ renderHook(() => useInstallationSetup())
+
+ // Get the registered callback
+ const startCallback = electronAPI.onInstallDependenciesStart.mock.calls[0][0]
+
+ act(() => {
+ startCallback()
+ })
+
+ expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
+ })
+
+ it('should handle install-dependencies-log event', () => {
+ renderHook(() => useInstallationSetup())
+
+ // Get the registered callback
+ const logCallback = electronAPI.onInstallDependenciesLog.mock.calls[0][0]
+ const logData = { type: 'stdout', data: 'Installing packages...' }
+
+ act(() => {
+ logCallback(logData)
+ })
+
+ expect(mockInstallationStore.addLog).toHaveBeenCalledWith({
+ type: 'stdout',
+ data: 'Installing packages...',
+ timestamp: expect.any(Date),
+ })
+ })
+
+ it('should handle install-dependencies-complete event with success', () => {
+ renderHook(() => useInstallationSetup())
+
+ // Get the registered callback
+ const completeCallback = electronAPI.onInstallDependenciesComplete.mock.calls[0][0]
+ const completeData = { success: true }
+
+ act(() => {
+ completeCallback(completeData)
+ })
+
+ expect(mockInstallationStore.setSuccess).toHaveBeenCalled()
+ expect(mockAuthStore.setInitState).toHaveBeenCalledWith('done')
+ })
+
+ it('should handle install-dependencies-complete event with failure', () => {
+ renderHook(() => useInstallationSetup())
+
+ // Get the registered callback
+ const completeCallback = electronAPI.onInstallDependenciesComplete.mock.calls[0][0]
+ const completeData = { success: false, error: 'Installation failed' }
+
+ act(() => {
+ completeCallback(completeData)
+ })
+
+ expect(mockInstallationStore.setError).toHaveBeenCalledWith('Installation failed')
+ expect(mockAuthStore.setInitState).not.toHaveBeenCalledWith('done')
+ })
+
+ it('should handle complete event without error message', () => {
+ renderHook(() => useInstallationSetup())
+
+ const completeCallback = electronAPI.onInstallDependenciesComplete.mock.calls[0][0]
+ const completeData = { success: false }
+
+ act(() => {
+ completeCallback(completeData)
+ })
+
+ expect(mockInstallationStore.setError).toHaveBeenCalledWith('Installation failed')
+ })
+ })
+
+ describe('Event Listener Cleanup', () => {
+ it('should remove all event listeners on unmount', () => {
+ const { unmount } = renderHook(() => useInstallationSetup())
+
+ unmount()
+
+ expect(electronAPI.removeAllListeners).toHaveBeenCalledWith('install-dependencies-start')
+ expect(electronAPI.removeAllListeners).toHaveBeenCalledWith('install-dependencies-log')
+ expect(electronAPI.removeAllListeners).toHaveBeenCalledWith('install-dependencies-complete')
+ })
+ })
+
+ describe('Test Scenarios Integration', () => {
+ it('should handle fresh installation scenario', async () => {
+ TestScenarios.freshInstall(electronAPI)
+
+ // Mock tool not installed
+ window.ipcRenderer.invoke = vi.fn().mockResolvedValue({
+ success: true,
+ isInstalled: false
+ })
+
+ renderHook(() => useInstallationSetup())
+
+ await vi.waitFor(() => {
+ expect(mockAuthStore.setInitState).toHaveBeenCalledWith('carousel')
+ })
+ })
+
+ it('should handle version update scenario', async () => {
+ TestScenarios.versionUpdate(electronAPI)
+
+ renderHook(() => useInstallationSetup())
+
+ // Simulate version update detection and installation start
+ electronAPI.simulateInstallationStart()
+
+ await vi.waitFor(() => {
+ expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
+ })
+ })
+
+ it('should handle venv removed scenario', async () => {
+ TestScenarios.venvRemoved(electronAPI)
+ electronAPI.mockState.isInstalling = true
+
+ renderHook(() => useInstallationSetup())
+
+ await vi.waitFor(() => {
+ expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
+ })
+ })
+
+ it('should handle installation in progress scenario', async () => {
+ TestScenarios.installationInProgress(electronAPI)
+
+ renderHook(() => useInstallationSetup())
+
+ await vi.waitFor(() => {
+ expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
+ })
+ })
+
+ it('should handle uvicorn startup with dependency installation', async () => {
+ TestScenarios.uvicornDepsInstall(electronAPI)
+
+ renderHook(() => useInstallationSetup())
+
+ // Simulate uvicorn detecting and installing dependencies
+ act(() => {
+ electronAPI.simulateUvicornStartup()
+ })
+
+ await vi.waitFor(() => {
+ expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
+ })
+
+ // Should receive logs and completion
+ await vi.waitFor(() => {
+ expect(mockInstallationStore.addLog).toHaveBeenCalled()
+ expect(mockInstallationStore.setSuccess).toHaveBeenCalled()
+ })
+ })
+ })
+
+ describe('Error Handling', () => {
+ it('should handle tool installation check failure', async () => {
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+
+ window.ipcRenderer.invoke = vi.fn().mockRejectedValue(new Error('IPC failed'))
+
+ renderHook(() => useInstallationSetup())
+
+ await vi.waitFor(() => {
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ '[useInstallationSetup] Tool installation check failed:',
+ expect.any(Error)
+ )
+ })
+
+ consoleErrorSpy.mockRestore()
+ })
+
+ it('should handle installation status check failure', async () => {
+ // Mock console.error to suppress expected error logs
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+
+ electronAPI.getInstallationStatus.mockRejectedValue(new Error('Status check failed'))
+
+ renderHook(() => useInstallationSetup())
+
+ // Should not crash, should handle the error gracefully
+ await vi.waitFor(() => {
+ expect(electronAPI.getInstallationStatus).toHaveBeenCalled()
+ })
+
+ // Wait for error to be logged
+ await vi.waitFor(() => {
+ expect(consoleErrorSpy).toHaveBeenCalled()
+ })
+
+ consoleErrorSpy.mockRestore()
+ })
+ })
+
+ describe('Multiple Hook Instances', () => {
+ it('should handle multiple hook instances without conflicts', () => {
+ const { result: hook1 } = renderHook(() => useInstallationSetup())
+ const { result: hook2 } = renderHook(() => useInstallationSetup())
+
+ // Both hooks should register listeners
+ expect(electronAPI.onInstallDependenciesStart).toHaveBeenCalledTimes(2)
+ expect(electronAPI.onInstallDependenciesLog).toHaveBeenCalledTimes(2)
+ expect(electronAPI.onInstallDependenciesComplete).toHaveBeenCalledTimes(2)
+ })
+ })
+
+ describe('State Dependencies', () => {
+ it('should react to initState changes', async () => {
+ mockAuthStore.initState = 'carousel'
+
+ const { rerender } = renderHook(() => useInstallationSetup())
+
+ // Change initState to 'done'
+ mockAuthStore.initState = 'done'
+ rerender()
+
+ // Should check tool installation again
+ await vi.waitFor(() => {
+ expect(window.ipcRenderer.invoke).toHaveBeenCalledWith('check-tool-installed')
+ })
+ })
+
+ it('should not set carousel state if initState is not done', async () => {
+ mockAuthStore.initState = 'loading'
+
+ window.ipcRenderer.invoke = vi.fn().mockResolvedValue({
+ success: true,
+ isInstalled: false
+ })
+
+ renderHook(() => useInstallationSetup())
+
+ await vi.waitFor(() => {
+ expect(window.ipcRenderer.invoke).toHaveBeenCalledWith('check-tool-installed')
+ })
+
+ // Should not call setInitState because initState is not 'done'
+ expect(mockAuthStore.setInitState).not.toHaveBeenCalledWith('carousel')
+ })
+ })
+
+ describe('Console Logging', () => {
+ it('should log installation status check', async () => {
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
+
+ renderHook(() => useInstallationSetup())
+
+ await vi.waitFor(() => {
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ '[useInstallationSetup] Installation status check:',
+ expect.any(Object)
+ )
+ })
+
+ consoleLogSpy.mockRestore()
+ })
+
+ it('should log when installation listeners are registered', () => {
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
+
+ renderHook(() => useInstallationSetup())
+
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ '[useInstallationSetup] Installation listeners registered'
+ )
+
+ consoleLogSpy.mockRestore()
+ })
+
+ it('should log install complete events', () => {
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
+
+ renderHook(() => useInstallationSetup())
+
+ const completeCallback = electronAPI.onInstallDependenciesComplete.mock.calls[0][0]
+
+ act(() => {
+ completeCallback({ success: true })
+ })
+
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ '[useInstallationSetup] Install complete event received:',
+ { success: true }
+ )
+
+ consoleLogSpy.mockRestore()
+ })
+ })
+})
\ No newline at end of file
diff --git a/test/unit/store/installationStore.test.ts b/test/unit/store/installationStore.test.ts
new file mode 100644
index 000000000..5db11d72a
--- /dev/null
+++ b/test/unit/store/installationStore.test.ts
@@ -0,0 +1,420 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
+import { act, renderHook } from '@testing-library/react'
+import { useInstallationStore, type InstallationState } from '../../../src/store/installationStore'
+import { setupElectronMocks, TestScenarios, type MockedElectronAPI } from '../../mocks/electronMocks'
+
+// Mock the authStore import since it's imported dynamically
+vi.mock('../../../src/store/authStore', () => ({
+ useAuthStore: {
+ getState: () => ({
+ setInitState: vi.fn()
+ })
+ }
+}))
+
+describe('Installation Store', () => {
+ let electronAPI: MockedElectronAPI
+ let mockSetInitState: ReturnType
+
+ beforeEach(async () => {
+ // Set up electron mocks
+ const mocks = setupElectronMocks()
+ electronAPI = mocks.electronAPI
+
+ // Mock the authStore
+ const { useAuthStore } = await import('../../../src/store/authStore')
+ mockSetInitState = vi.fn()
+ useAuthStore.getState = vi.fn().mockReturnValue({
+ setInitState: mockSetInitState
+ })
+
+ // Reset the store to initial state
+ useInstallationStore.getState().reset()
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ electronAPI.reset()
+ })
+
+ describe('Initial State', () => {
+ it('should have correct initial state', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ expect(result.current.state).toBe('idle')
+ expect(result.current.progress).toBe(20)
+ expect(result.current.logs).toEqual([])
+ expect(result.current.error).toBeUndefined()
+ expect(result.current.isVisible).toBe(false)
+ })
+ })
+
+ describe('State Transitions', () => {
+ it('should transition from idle to installing when startInstallation is called', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ expect(result.current.state).toBe('installing')
+ expect(result.current.progress).toBe(20)
+ expect(result.current.logs).toEqual([])
+ expect(result.current.error).toBeUndefined()
+ expect(result.current.isVisible).toBe(true)
+ })
+
+ it('should transition to completed when setSuccess is called', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ act(() => {
+ result.current.setSuccess()
+ })
+
+ expect(result.current.state).toBe('completed')
+ expect(result.current.progress).toBe(100)
+ })
+
+ it('should transition to error when setError is called', () => {
+ const { result } = renderHook(() => useInstallationStore())
+ const errorMessage = 'Installation failed'
+
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ act(() => {
+ result.current.setError(errorMessage)
+ })
+
+ expect(result.current.state).toBe('error')
+ expect(result.current.error).toBe(errorMessage)
+ expect(result.current.logs).toHaveLength(1)
+ expect(result.current.logs[0].type).toBe('stderr')
+ expect(result.current.logs[0].data).toBe(errorMessage)
+ })
+
+ it('should reset to installing state when retryInstallation is called', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ // First, set error state
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ act(() => {
+ result.current.setError('Some error')
+ })
+
+ expect(result.current.state).toBe('error')
+
+ // Then retry
+ act(() => {
+ result.current.retryInstallation()
+ })
+
+ expect(result.current.state).toBe('installing')
+ expect(result.current.logs).toEqual([])
+ expect(result.current.error).toBeUndefined()
+ expect(result.current.isVisible).toBe(true)
+ })
+ })
+
+ describe('Log Management', () => {
+ it('should add logs and update progress', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ const initialProgress = result.current.progress
+
+ act(() => {
+ result.current.addLog({
+ type: 'stdout',
+ data: 'Installing package...',
+ timestamp: new Date()
+ })
+ })
+
+ expect(result.current.logs).toHaveLength(1)
+ expect(result.current.logs[0].type).toBe('stdout')
+ expect(result.current.logs[0].data).toBe('Installing package...')
+ expect(result.current.progress).toBe(initialProgress + 5)
+ })
+
+ it('should not exceed 90% progress when adding logs', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ act(() => {
+ result.current.startInstallation()
+ })
+
+ // Add many logs to test progress cap
+ act(() => {
+ for (let i = 0; i < 20; i++) {
+ result.current.addLog({
+ type: 'stdout',
+ data: `Log entry ${i}`,
+ timestamp: new Date()
+ })
+ }
+ })
+
+ expect(result.current.progress).toBe(90)
+ expect(result.current.logs).toHaveLength(20)
+ })
+ })
+
+ describe('Installation Flow Integration', () => {
+ it('should handle successful installation flow', async () => {
+ TestScenarios.versionUpdate(electronAPI)
+
+ const { result } = renderHook(() => useInstallationStore())
+
+ // Start installation
+ await act(async () => {
+ await result.current.performInstallation()
+ })
+
+ // Wait for the mocked installation to complete
+ await vi.waitFor(() => {
+ expect(result.current.state).toBe('completed')
+ }, { timeout: 1000 })
+
+ expect(electronAPI.checkAndInstallDepsOnUpdate).toHaveBeenCalled()
+ expect(mockSetInitState).toHaveBeenCalledWith('done')
+ })
+
+ it('should handle installation failure', async () => {
+ TestScenarios.installationError(electronAPI)
+
+ const { result } = renderHook(() => useInstallationStore())
+
+ await act(async () => {
+ await result.current.performInstallation()
+ })
+
+ // Wait for the mocked installation to fail
+ await vi.waitFor(() => {
+ expect(result.current.state).toBe('error')
+ }, { timeout: 1000 })
+
+ expect(result.current.error).toBe('Installation failed')
+ })
+
+ it('should handle fresh installation scenario', async () => {
+ TestScenarios.freshInstall(electronAPI)
+
+ const { result } = renderHook(() => useInstallationStore())
+
+ await act(async () => {
+ await result.current.performInstallation()
+ })
+
+ await vi.waitFor(() => {
+ expect(result.current.state).toBe('completed')
+ }, { timeout: 1000 })
+
+ expect(electronAPI.checkAndInstallDepsOnUpdate).toHaveBeenCalled()
+ })
+ })
+
+ describe('Log Export', () => {
+ it('should export logs successfully', async () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ // Mock window.location.href
+ const originalLocation = window.location
+ Object.defineProperty(window, 'location', {
+ value: { href: '' },
+ writable: true
+ })
+
+ // Mock alert
+ const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {})
+
+ await act(async () => {
+ await result.current.exportLog()
+ })
+
+ expect(electronAPI.exportLog).toHaveBeenCalled()
+ expect(alertSpy).toHaveBeenCalledWith('Log saved: /mock/path/to/log.txt')
+ expect(window.location.href).toBe('https://github.com/eigent-ai/eigent/issues/new/choose')
+
+ // Restore
+ Object.defineProperty(window, 'location', {
+ value: originalLocation,
+ writable: true
+ })
+ alertSpy.mockRestore()
+ })
+
+ it('should handle export failure', async () => {
+ electronAPI.exportLog.mockResolvedValue({
+ success: false,
+ error: 'Export failed'
+ })
+
+ const { result } = renderHook(() => useInstallationStore())
+ const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {})
+
+ await act(async () => {
+ await result.current.exportLog()
+ })
+
+ expect(alertSpy).toHaveBeenCalledWith('Export cancelled: Export failed')
+ alertSpy.mockRestore()
+ })
+ })
+
+ describe('Computed Selectors', () => {
+ it('useLatestLog should return the most recent log', () => {
+ const { result: storeResult } = renderHook(() => useInstallationStore())
+ const { result: latestLogResult } = renderHook(() => useInstallationStore((state: any) =>
+ state.logs[state.logs.length - 1]
+ ))
+
+ expect(latestLogResult.current).toBeUndefined()
+
+ act(() => {
+ storeResult.current.startInstallation()
+ storeResult.current.addLog({
+ type: 'stdout',
+ data: 'First log',
+ timestamp: new Date()
+ })
+ storeResult.current.addLog({
+ type: 'stderr',
+ data: 'Latest log',
+ timestamp: new Date()
+ })
+ })
+
+ expect(latestLogResult.current.data).toBe('Latest log')
+ expect(latestLogResult.current.type).toBe('stderr')
+ })
+
+ it('useInstallationStatus should return correct status', () => {
+ const { result: storeResult } = renderHook(() => useInstallationStore())
+ const { result: statusResult } = renderHook(() => {
+ const state = useInstallationStore((state: any) => state.state)
+ const isVisible = useInstallationStore((state: any) => state.isVisible)
+
+ return {
+ isInstalling: state === 'installing',
+ installationState: state,
+ shouldShowInstallScreen: isVisible && state !== 'completed',
+ isInstallationComplete: state === 'completed',
+ canRetry: state === 'error',
+ }
+ })
+
+ // Initial state
+ expect(statusResult.current.isInstalling).toBe(false)
+ expect(statusResult.current.installationState).toBe('idle')
+ expect(statusResult.current.shouldShowInstallScreen).toBe(false)
+ expect(statusResult.current.isInstallationComplete).toBe(false)
+ expect(statusResult.current.canRetry).toBe(false)
+
+ // Installing state
+ act(() => {
+ storeResult.current.startInstallation()
+ })
+
+ expect(statusResult.current.isInstalling).toBe(true)
+ expect(statusResult.current.shouldShowInstallScreen).toBe(true)
+ expect(statusResult.current.canRetry).toBe(false)
+
+ // Error state
+ act(() => {
+ storeResult.current.setError('Some error')
+ })
+
+ expect(statusResult.current.isInstalling).toBe(false)
+ expect(statusResult.current.canRetry).toBe(true)
+
+ // Completed state
+ act(() => {
+ storeResult.current.setSuccess()
+ })
+
+ expect(statusResult.current.isInstallationComplete).toBe(true)
+ expect(statusResult.current.shouldShowInstallScreen).toBe(false)
+ })
+ })
+
+ describe('Edge Cases', () => {
+ it('should handle multiple rapid state changes', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ act(() => {
+ result.current.startInstallation()
+ result.current.setError('Error 1')
+ result.current.retryInstallation()
+ result.current.setSuccess()
+ })
+
+ expect(result.current.state).toBe('completed')
+ expect(result.current.progress).toBe(100)
+ })
+
+ it('should handle visibility changes correctly', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ expect(result.current.isVisible).toBe(false)
+
+ act(() => {
+ result.current.setVisible(true)
+ })
+
+ expect(result.current.isVisible).toBe(true)
+
+ act(() => {
+ result.current.completeSetup()
+ })
+
+ expect(result.current.state).toBe('completed')
+ expect(result.current.isVisible).toBe(false)
+ })
+
+ it('should handle manual progress updates', () => {
+ const { result } = renderHook(() => useInstallationStore())
+
+ act(() => {
+ result.current.updateProgress(75)
+ })
+
+ expect(result.current.progress).toBe(75)
+ })
+ })
+
+ describe('Installation State Sequence', () => {
+ it('should follow correct state sequence for normal installation', async () => {
+ const { result } = renderHook(() => useInstallationStore())
+ const states: InstallationState[] = []
+
+ // Subscribe to state changes
+ useInstallationStore.subscribe((state: any) => {
+ states.push(state.state)
+ })
+
+ await act(async () => {
+ await result.current.performInstallation()
+ })
+
+ await vi.waitFor(() => {
+ expect(result.current.state).toBe('completed')
+ }, { timeout: 1000 })
+
+ // Should have progressed through: idle -> installing -> completed
+ expect(states).toContain('installing')
+ expect(states).toContain('completed')
+ })
+ })
+})
\ No newline at end of file
diff --git a/vitest.config.ts b/vitest.config.ts
index afc219c96..5592cc87c 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -23,7 +23,6 @@ export default defineConfig({
'test/',
'dist/',
'dist-electron/',
- 'electron/',
'build/',
'**/*.d.ts',
'**/*.config.*',